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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-swidget
3
- Version: 0.0.31
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
+ ```
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-swidget"
3
- version = "0.0.31"
3
+ version = "1.0.0"
4
4
  description = "Python API for Swidget smart devices"
5
5
  license = "GPL-3.0-or-later"
6
6
  authors = ["Swidget"]
@@ -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
- ssl=False,
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
- ssl=False,
89
+ use_https=http_only,
91
90
  use_websockets=False)
91
+ await dev.update()
92
92
 
93
- await dev.update()
94
- ctx.obj = dev
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
- @pass_dev
102
- def wifi(dev):
107
+ def wifi():
103
108
  """Commands to control wifi settings."""
104
109
 
110
+
105
111
  @wifi.command()
106
- @click.argument("ssid")
107
- @click.option("--password", prompt=True, hide_input=True)
108
- @click.option("--keytype", default=3)
109
- @pass_dev
110
- async def join(dev: SwidgetDevice, ssid, password, keytype):
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.echo(f"Asking the device to connect to {ssid}..")
113
- res = await dev.wifi_join(ssid, password, keytype=keytype)
114
- click.echo(
115
- f"Response: {res} - if the device is not able to join the network, it will revert back to its previous state."
116
- )
117
-
118
- return res
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
- for info_name, info_data in dev.realtime_values.items():
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, ssl: bool, use_websockets: bool) -> SwidgetDevice:
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
- swidget_device = SwidgetDevice(host, token_name, password, ssl, use_websockets)
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
- dev = device_class(host, token_name, password, False, use_websockets)
79
- await dev.update()
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, ssl=False, use_websockets=True):
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
- self.ssl = ssl
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
- connector = TCPConnector(force_close=True)
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
- return self._websocket
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 stop(self):
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
- if self._websocket is not None:
56
- self._websocket.stop()
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
- async with self._session.get(
68
- url=f"https://{self.ip_address}/api/v1/summary", ssl=self.ssl
69
- ) as response:
70
- summary = await response.json()
71
- await self.process_summary(summary)
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"https://{self.ip_address}/api/v1/name", ssl=self.ssl
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
- async with self._session.get(
103
- url=f"https://{self.ip_address}/api/v1/state", ssl=self.ssl
104
- ) as response:
105
- state = await response.json()
106
- await self.process_state(state)
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
- await self.get_summary()
128
- await self.get_state()
129
- await self.get_friendly_name()
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
- data = json.dumps({"type":"config","request_id":"abcd", "payload": payload})
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"https://{self.ip_address}/api/v1/command",
150
- ssl=self.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"https://{self.ip_address}/ping",
166
- ssl=self.ssl
221
+ url=f"{self.uri_scheme}://{self.ip_address}/ping",
222
+ ssl=False
167
223
  ) as response:
168
- return response.text
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"https://{self.ip_address}/blink",
180
- ssl=self.ssl
236
+ url=f"{self.uri_scheme}://{self.ip_address}/blink",
237
+ ssl=False
181
238
  ) as response:
182
- return response.text
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 False
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, ssl: bool, use_websockets: bool) -> None:
16
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
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, ssl: bool, use_websockets: bool) -> None:
10
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
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, ssl: bool, use_websockets: bool) -> None:
10
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
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, ssl: bool, use_websockets: bool) -> None:
10
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
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")
@@ -1,2 +0,0 @@
1
- # python-swidget
2
- A library to manage Swidget smart devices
@@ -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()