python-swidget 0.0.32__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.32 → python_swidget-1.0.0}/PKG-INFO +19 -1
- python_swidget-1.0.0/README.md +21 -0
- {python_swidget-0.0.32 → python_swidget-1.0.0}/pyproject.toml +1 -1
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/cli.py +53 -6
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/discovery.py +12 -4
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/provision.py +1 -0
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/swidgetdevice.py +127 -38
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/swidgetdimmer.py +5 -4
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/swidgetoutlet.py +2 -2
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/swidgetswitch.py +2 -2
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/swidgettimerswitch.py +6 -2
- python_swidget-1.0.0/swidget/websocket.py +99 -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.0}/swidget/__init__.py +0 -0
- {python_swidget-0.0.32 → python_swidget-1.0.0}/swidget/exceptions.py +0 -0
- {python_swidget-0.0.32 → 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
|
|
@@ -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)
|
|
@@ -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()
|
|
@@ -39,7 +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
|
+
=======
|
|
42
44
|
_LOGGER.debug(f"Swidget device '{friendly_name}' at {ip_address}")
|
|
45
|
+
>>>>>>> 690a0c560a8b2ff39245d1a8354ced968d79e5de
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
async def discover_devices(timeout=RESPONSE_SEC):
|
|
@@ -65,21 +68,26 @@ async def discover_devices(timeout=RESPONSE_SEC):
|
|
|
65
68
|
return devices
|
|
66
69
|
|
|
67
70
|
|
|
68
|
-
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:
|
|
69
72
|
"""Discover a single device by the given IP address.
|
|
70
73
|
|
|
71
74
|
:param host: Hostname of device to query
|
|
72
75
|
:rtype: SwidgetDevice
|
|
73
76
|
:return: Object for querying/controlling found device.
|
|
74
77
|
"""
|
|
75
|
-
|
|
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")
|
|
76
81
|
await swidget_device.get_summary()
|
|
77
82
|
device_type = swidget_device.device_type
|
|
83
|
+
_LOGGER.debug(f"{host} is of type {device_type}")
|
|
78
84
|
await swidget_device.stop()
|
|
79
85
|
|
|
86
|
+
_LOGGER.debug(f"Creating new device class of type: {device_type}")
|
|
80
87
|
device_class = _get_device_class(device_type)
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
_LOGGER.debug(f"{device_class}")
|
|
89
|
+
dev = device_class(host, token_name, password, use_https, use_websockets)
|
|
90
|
+
await dev.start()
|
|
83
91
|
return dev
|
|
84
92
|
|
|
85
93
|
|
|
@@ -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,37 +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
|
|
|
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
|
+
|
|
53
70
|
async def stop(self):
|
|
54
71
|
"""Stop the websocket."""
|
|
72
|
+
_LOGGER.debug("SwidgetDevice.stop()")
|
|
55
73
|
if hasattr(self, '_websocket'):
|
|
56
74
|
await self._websocket.close()
|
|
57
75
|
await self._session.close()
|
|
58
76
|
|
|
59
77
|
async def message_callback(self, message):
|
|
60
78
|
"""Entrypoint for a websocket callback"""
|
|
79
|
+
_LOGGER.debug("SwidgetDevice.message_callback() called")
|
|
61
80
|
if message["request_id"] == "summary":
|
|
81
|
+
_LOGGER.debug("Calling SwidgetDevice.process_summary()")
|
|
62
82
|
await self.process_summary(message)
|
|
63
83
|
elif message["request_id"] == "state" or message["request_id"] == "DYNAMIC_UPDATE" or message["request_id"] == "command":
|
|
84
|
+
_LOGGER.debug("Calling SwidgetDevice.process_state()")
|
|
64
85
|
await self.process_state(message)
|
|
86
|
+
else:
|
|
87
|
+
_LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
|
|
65
88
|
|
|
66
89
|
async def get_summary(self):
|
|
67
90
|
"""Get a summary of the device over HTTP"""
|
|
68
|
-
|
|
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")
|
|
69
97
|
async with self._session.get(
|
|
70
|
-
url=f"
|
|
98
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
|
|
71
99
|
) as response:
|
|
72
100
|
summary = await response.json()
|
|
73
101
|
await self.process_summary(summary)
|
|
74
|
-
except:
|
|
75
|
-
raise SwidgetException("Unable to connect to device")
|
|
76
102
|
|
|
77
103
|
async def process_summary(self, summary):
|
|
78
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}")
|
|
79
107
|
self.model = summary["model"]
|
|
80
108
|
self.mac_address = summary["mac"]
|
|
81
109
|
self.version = summary["version"]
|
|
@@ -89,9 +117,10 @@ class SwidgetDevice:
|
|
|
89
117
|
self._last_update = int(time.time())
|
|
90
118
|
|
|
91
119
|
async def get_friendly_name(self):
|
|
120
|
+
_LOGGER.debug("SwidgetDevice.get_friendly_name() called")
|
|
92
121
|
try:
|
|
93
122
|
async with self._session.get(
|
|
94
|
-
url=f"
|
|
123
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
|
|
95
124
|
) as response:
|
|
96
125
|
name = await response.json()
|
|
97
126
|
except Exception:
|
|
@@ -99,24 +128,33 @@ class SwidgetDevice:
|
|
|
99
128
|
await self.process_friendly_name(name['name'])
|
|
100
129
|
|
|
101
130
|
async def process_friendly_name(self, name):
|
|
131
|
+
_LOGGER.debug("SwidgetDevice.process_friendly_name() called")
|
|
102
132
|
self._friendly_name = name
|
|
133
|
+
self._last_update = int(time.time())
|
|
103
134
|
|
|
104
135
|
async def get_state(self):
|
|
105
136
|
""" Get the state of the device over HTTP"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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)
|
|
111
148
|
|
|
112
149
|
async def process_state(self, state):
|
|
113
150
|
""" Process any information about the state of the device or insert"""
|
|
114
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}")
|
|
115
154
|
try:
|
|
116
155
|
self.rssi = state["connection"]["rssi"]
|
|
117
156
|
except:
|
|
118
157
|
pass
|
|
119
|
-
|
|
120
158
|
for assembly in self.assemblies:
|
|
121
159
|
for id, component in self.assemblies[assembly].components.items():
|
|
122
160
|
try:
|
|
@@ -126,37 +164,49 @@ class SwidgetDevice:
|
|
|
126
164
|
self._last_update = int(time.time())
|
|
127
165
|
|
|
128
166
|
async def update(self):
|
|
167
|
+
_LOGGER.debug("SwidgetDevice.update() called")
|
|
129
168
|
if self._last_update is None:
|
|
130
169
|
_LOGGER.debug("Performing the initial update to obtain sysinfo")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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()
|
|
135
180
|
|
|
136
181
|
async def send_config(self, payload: dict):
|
|
137
|
-
|
|
182
|
+
_LOGGER.debug("SwidgetDevice.send_config() called")
|
|
183
|
+
data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
|
|
138
184
|
await self._websocket.send_str(data)
|
|
139
185
|
|
|
140
186
|
async def send_command(
|
|
141
187
|
self, assembly: str, component: str, function: str, command: dict
|
|
142
188
|
):
|
|
189
|
+
_LOGGER.debug("SwidgetDevice.send_command() called")
|
|
143
190
|
"""Send a command to the Swidget device either using a HTTP call or the existing websocket"""
|
|
144
191
|
data = {assembly: {"components": {component: {function: command}}}}
|
|
145
|
-
|
|
192
|
+
_LOGGER.debug(f"Command to send: {data}")
|
|
146
193
|
if self.use_websockets:
|
|
194
|
+
_LOGGER.debug("In websocket mode. Sending command over websocket")
|
|
147
195
|
data = json.dumps({"type": "command",
|
|
148
196
|
"request_id": "command",
|
|
149
197
|
"payload": data
|
|
150
198
|
})
|
|
151
199
|
await self._websocket.send_str(data)
|
|
152
200
|
else:
|
|
201
|
+
_LOGGER.debug("NOT in websocket mode, sending command over HTTP")
|
|
153
202
|
async with self._session.post(
|
|
154
|
-
url=f"
|
|
155
|
-
ssl=
|
|
203
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/command",
|
|
204
|
+
ssl=False,
|
|
156
205
|
data=json.dumps(data),
|
|
157
206
|
) as response:
|
|
158
207
|
state = await response.json()
|
|
159
208
|
|
|
209
|
+
# Do a hard set of the new state of the device. May change this in the future
|
|
160
210
|
function_value = state[assembly]["components"][component][function]
|
|
161
211
|
self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
|
|
162
212
|
|
|
@@ -165,12 +215,13 @@ class SwidgetDevice:
|
|
|
165
215
|
|
|
166
216
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
167
217
|
"""
|
|
218
|
+
_LOGGER.debug("SwidgetDevice.ping() called")
|
|
168
219
|
try:
|
|
169
220
|
async with self._session.get(
|
|
170
|
-
url=f"
|
|
171
|
-
ssl=
|
|
221
|
+
url=f"{self.uri_scheme}://{self.ip_address}/ping",
|
|
222
|
+
ssl=False
|
|
172
223
|
) as response:
|
|
173
|
-
return response.
|
|
224
|
+
return response.status
|
|
174
225
|
except:
|
|
175
226
|
raise SwidgetException
|
|
176
227
|
|
|
@@ -179,12 +230,13 @@ class SwidgetDevice:
|
|
|
179
230
|
|
|
180
231
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
181
232
|
"""
|
|
233
|
+
_LOGGER.debug("SwidgetDevice.blink() called")
|
|
182
234
|
try:
|
|
183
235
|
async with self._session.get(
|
|
184
|
-
url=f"
|
|
185
|
-
ssl=
|
|
236
|
+
url=f"{self.uri_scheme}://{self.ip_address}/blink",
|
|
237
|
+
ssl=False
|
|
186
238
|
) as response:
|
|
187
|
-
return response.
|
|
239
|
+
return await response.json()
|
|
188
240
|
except:
|
|
189
241
|
raise SwidgetException
|
|
190
242
|
|
|
@@ -193,12 +245,13 @@ class SwidgetDevice:
|
|
|
193
245
|
|
|
194
246
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
195
247
|
"""
|
|
248
|
+
_LOGGER.debug("SwidgetDevice.enable_debug_server() called")
|
|
196
249
|
try:
|
|
197
250
|
async with self._session.get(
|
|
198
|
-
url=f"
|
|
199
|
-
ssl=
|
|
251
|
+
url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
|
|
252
|
+
ssl=False
|
|
200
253
|
) as response:
|
|
201
|
-
return response.
|
|
254
|
+
return await response.json()
|
|
202
255
|
except:
|
|
203
256
|
raise SwidgetException
|
|
204
257
|
|
|
@@ -210,10 +263,43 @@ class SwidgetDevice:
|
|
|
210
263
|
try:
|
|
211
264
|
|
|
212
265
|
async with self._session.delete(
|
|
213
|
-
url=f"
|
|
214
|
-
ssl=
|
|
266
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset",
|
|
267
|
+
ssl=False
|
|
215
268
|
) as response:
|
|
216
|
-
return 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
|
|
217
303
|
except:
|
|
218
304
|
raise SwidgetException
|
|
219
305
|
|
|
@@ -329,7 +415,7 @@ class SwidgetDevice:
|
|
|
329
415
|
@property
|
|
330
416
|
def is_dimmable(self) -> bool:
|
|
331
417
|
"""Return True if the device is dimmable."""
|
|
332
|
-
return
|
|
418
|
+
return self.is_dimmer
|
|
333
419
|
|
|
334
420
|
@property # type: ignore
|
|
335
421
|
def friendly_name(self) -> str:
|
|
@@ -346,11 +432,14 @@ class SwidgetDevice:
|
|
|
346
432
|
|
|
347
433
|
async def turn_on(self):
|
|
348
434
|
"""Turn the device on."""
|
|
435
|
+
_LOGGER.debug("SwidgetDevice.turn_on() called")
|
|
349
436
|
await self.send_command(
|
|
350
437
|
assembly="host", component="0", function="toggle", command={"state": "on"}
|
|
351
438
|
)
|
|
439
|
+
|
|
352
440
|
async def turn_off(self):
|
|
353
441
|
"""Turn the device off."""
|
|
442
|
+
_LOGGER.debug("SwidgetDevice.turn_off() called")
|
|
354
443
|
await self.send_command(
|
|
355
444
|
assembly="host", component="0", function="toggle", command={"state": "off"}
|
|
356
445
|
)
|
|
@@ -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.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
|