python-swidget 1.0.0__tar.gz → 1.0.2__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: 1.0.0
3
+ Version: 1.0.2
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
@@ -21,10 +21,12 @@ Requires-Dist: importlib-metadata
21
21
  Requires-Dist: m2r (>=0,<1) ; extra == "docs"
22
22
  Requires-Dist: mistune (<2.0.0) ; extra == "docs"
23
23
  Requires-Dist: pydantic (>=1,<2)
24
+ Requires-Dist: requests (==2.31.0)
24
25
  Requires-Dist: sphinx (>=4,<5) ; extra == "docs"
25
26
  Requires-Dist: sphinx_rtd_theme (>=0,<1) ; extra == "docs"
26
27
  Requires-Dist: sphinxcontrib-programoutput (>=0,<1) ; extra == "docs"
27
28
  Requires-Dist: ssdp (==1.1.1)
29
+ Requires-Dist: types-requests (==2.31.0.20240406)
28
30
  Project-URL: Repository, https://github.com/swidget/python-swidget
29
31
  Description-Content-Type: text/markdown
30
32
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-swidget"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "Python API for Swidget smart devices"
5
5
  license = "GPL-3.0-or-later"
6
6
  authors = ["Swidget"]
@@ -22,6 +22,8 @@ importlib-metadata = "*"
22
22
  asyncclick = ">=8"
23
23
  pydantic = "^1"
24
24
  ssdp = "1.1.1"
25
+ requests = "2.31.0"
26
+ types-requests = "2.31.0.20240406"
25
27
 
26
28
  # required only for docs
27
29
  sphinx = { version = "^4", optional = true }
@@ -1,8 +1,8 @@
1
1
  """python-swidget cli tool."""
2
2
  import logging
3
3
  import sys
4
+ from typing import Any, cast
4
5
  from pprint import pformat as pf
5
- from typing import cast
6
6
 
7
7
  import asyncclick as click
8
8
  from contextlib import asynccontextmanager
@@ -25,8 +25,6 @@ TYPE_TO_CLASS = {
25
25
  "pana_switch": SwidgetTimerSwitch
26
26
  }
27
27
 
28
- click.anyio_backend = "asyncio"
29
-
30
28
 
31
29
  pass_dev = click.make_pass_decorator(SwidgetDevice)
32
30
 
@@ -72,7 +70,7 @@ async def cli(ctx, host, password, debug, http_only, type):
72
70
  if ctx.invoked_subcommand == "discover" or ctx.invoked_subcommand == "wifi":
73
71
  return
74
72
  if host is None:
75
- click.echo("No host name given, trying discovery..")
73
+ click.echo("No hostname or IP given, trying discovery..")
76
74
  await ctx.invoke(discover)
77
75
  return
78
76
  if type is not None:
@@ -127,7 +125,7 @@ def join(ssid, network_password, secret_key, friendly_name):
127
125
 
128
126
 
129
127
  @cli.command()
130
- @click.option("--timeout", default=5, required=False)
128
+ @click.option("--timeout", default=10, required=False)
131
129
  @click.pass_context
132
130
  async def discover(ctx, timeout):
133
131
  """Discover devices in the network."""
@@ -193,31 +191,25 @@ async def state(dev: SwidgetDevice):
193
191
  @click.argument("command")
194
192
  async def raw_command(dev: SwidgetDevice, assembly, component, function, command):
195
193
  """Run a raw command on the device."""
196
- import ast
197
-
198
- if parameters is not None:
199
- parameters = ast.literal_eval(parameters)
200
-
201
- res = await dev.send_command(assembly, component, function, command)
202
-
203
- click.echo(res)
204
- return res
194
+ await dev.send_command(assembly, component, function, command)
195
+ click.echo("Command sent")
205
196
 
206
197
 
207
198
  @cli.command()
208
199
  @click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
209
200
  @pass_dev
210
- async def brightness(dev: SwidgetDimmer, brightness: int):
201
+ async def brightness(dev: SwidgetDevice, brightness: Any=None):
202
+ dimmer_dev = cast(SwidgetDimmer, dev)
211
203
  """Get or set brightness."""
212
- if not dev.is_dimmer:
204
+ if not dimmer_dev.is_dimmer:
213
205
  click.echo("This device does not support brightness.")
214
206
  return
215
207
 
216
208
  if brightness is None:
217
- click.echo(f"Brightness: {dev.brightness}")
209
+ click.echo(f"Brightness: {dimmer_dev.brightness}")
218
210
  else:
219
211
  click.echo(f"Setting brightness to {brightness}")
220
- return await dev.set_brightness(brightness)
212
+ return await dimmer_dev.set_brightness(brightness)
221
213
 
222
214
 
223
215
  @cli.command()
@@ -1,11 +1,10 @@
1
1
  import asyncio
2
- import json
3
2
  import logging
4
3
  import socket
5
- from typing import Awaitable, Callable, Dict, Optional, Type, cast
4
+ from typing import Any, Type
6
5
  from urllib.parse import urlparse
7
6
 
8
- import ssdp
7
+ import ssdp # type: ignore
9
8
 
10
9
  from swidget.swidgetdevice import DeviceType, SwidgetDevice
11
10
  from .swidgetdimmer import SwidgetDimmer
@@ -39,10 +38,7 @@ class SwidgetProtocol(ssdp.SimpleServiceDiscoveryProtocol):
39
38
  insert_type = headers["SERVER"].split(" ")[1].split("+")[1].split("/")[0]
40
39
  friendly_name = headers["SERVER"].split("/")[2].strip('"')
41
40
  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
41
+ _LOGGER.debug(f"Swidget device '{friendly_name}' at {ip_address}. {device_type}/{insert_type}")
46
42
 
47
43
 
48
44
  async def discover_devices(timeout=RESPONSE_SEC):
@@ -68,7 +64,7 @@ async def discover_devices(timeout=RESPONSE_SEC):
68
64
  return devices
69
65
 
70
66
 
71
- async def discover_single(host: str, token_name: str, password: str, use_https: bool, use_websockets: bool) -> SwidgetDevice:
67
+ async def discover_single(host: str, token_name: str, password: str, use_https: bool, use_websockets: bool) -> Any:
72
68
  """Discover a single device by the given IP address.
73
69
 
74
70
  :param host: Hostname of device to query
@@ -91,16 +87,16 @@ async def discover_single(host: str, token_name: str, password: str, use_https:
91
87
  return dev
92
88
 
93
89
 
94
- def _get_device_class(device_type: str) -> Type[SwidgetDevice]:
90
+ def _get_device_class(device_type: DeviceType) -> Type[SwidgetDevice]:
95
91
  """Find SmartDevice subclass for device described by passed data."""
96
- if device_type == "outlet":
92
+ if device_type == DeviceType.Outlet:
97
93
  return SwidgetOutlet
98
- elif device_type == "switch":
94
+ elif device_type == DeviceType.Switch:
99
95
  return SwidgetSwitch
100
- elif device_type == "dimmer":
96
+ elif device_type == DeviceType.Dimmer:
101
97
  return SwidgetDimmer
102
- elif device_type == "pana_switch": # This is the timer switch
98
+ elif device_type == DeviceType.TimerSwitch: # This is the timer switch
103
99
  return SwidgetTimerSwitch
104
- elif device_type == "relay_switch":
100
+ elif device_type == DeviceType.RelaySwitch:
105
101
  return SwidgetSwitch
106
102
  raise SwidgetException("Unknown device type: %s" % device_type)
@@ -5,7 +5,7 @@ import time
5
5
  from aiohttp import ClientSession, TCPConnector
6
6
  import asyncio
7
7
  from enum import Enum
8
- from typing import Dict, List, Set
8
+ from typing import Any, Dict, List
9
9
 
10
10
  from .exceptions import SwidgetException
11
11
  from .websocket import SwidgetWebsocket
@@ -24,21 +24,21 @@ class DeviceType(Enum):
24
24
 
25
25
 
26
26
  class SwidgetDevice:
27
- def __init__(self, host, token_name, secret_key, use_https=True, use_websockets=True):
27
+ def __init__(self, host, token_name, secret_key, use_https=True, use_websockets=True) -> None:
28
28
  self.token_name = token_name
29
29
  self.ip_address = host
30
-
31
30
  self.use_https = use_https
32
31
  self.uri_scheme = 'https' if self.use_https is True else 'http'
33
32
  self.secret_key = secret_key
34
33
  self.use_websockets = use_websockets
35
34
  self.device_type = DeviceType.Unknown
36
35
  self._friendly_name = "Unknown Swidget Device"
36
+ self.assemblies: Dict[Any, Any] = dict()
37
37
  headers = {self.token_name: self.secret_key,
38
38
  'Connection': 'keep-alive'}
39
39
  connector = TCPConnector(verify_ssl=False, force_close=True)
40
40
  self._session = ClientSession(headers=headers, connector=connector)
41
- self._last_update = None
41
+ self._last_update: int = 0
42
42
  if self.use_websockets:
43
43
  self._websocket = SwidgetWebsocket(
44
44
  host=self.ip_address,
@@ -47,34 +47,37 @@ class SwidgetDevice:
47
47
  callback=self.message_callback,
48
48
  session=self._session)
49
49
 
50
- def get_websocket(self):
50
+ def get_websocket(self) -> SwidgetWebsocket | None:
51
51
  if self.use_websockets:
52
52
  return self._websocket
53
53
  return None
54
54
 
55
- def set_countdown_timer(self, minutes):
55
+ def set_countdown_timer(self, minutes) -> Any:
56
56
  raise NotImplementedError()
57
57
 
58
- async def connect(self):
58
+ async def connect(self) -> None:
59
59
  await self._websocket.connect()
60
60
 
61
- async def start(self):
61
+ async def start(self) -> None:
62
62
  """Start the websocket."""
63
63
  _LOGGER.debug("SwidgetDevice.start()")
64
64
  if self.use_websockets:
65
65
  _LOGGER.debug("Calling self._websocket.connect()")
66
66
  await self._websocket.connect()
67
- _LOGGER.debug("Calling self._websocket.listen() ")
68
- asyncio.create_task(self._websocket.listen())
67
+ _LOGGER.debug("Calling self.update() ")
68
+ await self.update()
69
69
 
70
- async def stop(self):
70
+ async def stop(self) -> None:
71
71
  """Stop the websocket."""
72
72
  _LOGGER.debug("SwidgetDevice.stop()")
73
73
  if hasattr(self, '_websocket'):
74
74
  await self._websocket.close()
75
75
  await self._session.close()
76
76
 
77
- async def message_callback(self, message):
77
+ async def close(self) -> None:
78
+ await self.stop()
79
+
80
+ async def message_callback(self, message) -> None:
78
81
  """Entrypoint for a websocket callback"""
79
82
  _LOGGER.debug("SwidgetDevice.message_callback() called")
80
83
  if message["request_id"] == "summary":
@@ -86,7 +89,7 @@ class SwidgetDevice:
86
89
  else:
87
90
  _LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
88
91
 
89
- async def get_summary(self):
92
+ async def get_summary(self) -> None:
90
93
  """Get a summary of the device over HTTP"""
91
94
  _LOGGER.debug("SwidgetDevice.get_summary() called")
92
95
  if self.use_websockets:
@@ -100,7 +103,7 @@ class SwidgetDevice:
100
103
  summary = await response.json()
101
104
  await self.process_summary(summary)
102
105
 
103
- async def process_summary(self, summary):
106
+ async def process_summary(self, summary) -> None:
104
107
  """ Process the data around the summary of the device"""
105
108
  _LOGGER.debug("SwidgetDevice.process_summary() called")
106
109
  _LOGGER.debug(f"Summary to process: {summary}")
@@ -111,12 +114,12 @@ class SwidgetDevice:
111
114
  "host": SwidgetAssembly(summary["host"]),
112
115
  "insert": SwidgetAssembly(summary["insert"]),
113
116
  }
114
- self.device_type = self.assemblies['host'].type
117
+ self.device_type = DeviceType(self.assemblies['host'].type)
115
118
  self.insert_type = self.assemblies['insert'].type
116
119
  self.id = self.assemblies['host'].id
117
120
  self._last_update = int(time.time())
118
121
 
119
- async def get_friendly_name(self):
122
+ async def get_friendly_name(self) -> None:
120
123
  _LOGGER.debug("SwidgetDevice.get_friendly_name() called")
121
124
  try:
122
125
  async with self._session.get(
@@ -127,12 +130,12 @@ class SwidgetDevice:
127
130
  name = {"name": f"Swidget {self.device_type} w/{self.insert_type} insert"}
128
131
  await self.process_friendly_name(name['name'])
129
132
 
130
- async def process_friendly_name(self, name):
133
+ async def process_friendly_name(self, name) -> None:
131
134
  _LOGGER.debug("SwidgetDevice.process_friendly_name() called")
132
135
  self._friendly_name = name
133
136
  self._last_update = int(time.time())
134
137
 
135
- async def get_state(self):
138
+ async def get_state(self) -> None:
136
139
  """ Get the state of the device over HTTP"""
137
140
  _LOGGER.debug("SwidgetDevice.get_state() called")
138
141
  if self.use_websockets:
@@ -146,7 +149,7 @@ class SwidgetDevice:
146
149
  state = await response.json()
147
150
  await self.process_state(state)
148
151
 
149
- async def process_state(self, state):
152
+ async def process_state(self, state) -> None:
150
153
  """ Process any information about the state of the device or insert"""
151
154
  # State is not always in the state (during callback)
152
155
  _LOGGER.debug("SwidgetDevice.process_state() called")
@@ -163,9 +166,9 @@ class SwidgetDevice:
163
166
  pass
164
167
  self._last_update = int(time.time())
165
168
 
166
- async def update(self):
169
+ async def update(self) -> None:
167
170
  _LOGGER.debug("SwidgetDevice.update() called")
168
- if self._last_update is None:
171
+ if self._last_update == 0:
169
172
  _LOGGER.debug("Performing the initial update to obtain sysinfo")
170
173
  await self.get_summary()
171
174
  await self.get_state()
@@ -178,25 +181,25 @@ class SwidgetDevice:
178
181
  await self.get_summary()
179
182
  await self.get_state()
180
183
 
181
- async def send_config(self, payload: dict):
184
+ async def send_config(self, payload: dict) -> None:
182
185
  _LOGGER.debug("SwidgetDevice.send_config() called")
183
186
  data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
184
187
  await self._websocket.send_str(data)
185
188
 
186
189
  async def send_command(
187
190
  self, assembly: str, component: str, function: str, command: dict
188
- ):
191
+ ) -> None:
189
192
  _LOGGER.debug("SwidgetDevice.send_command() called")
190
193
  """Send a command to the Swidget device either using a HTTP call or the existing websocket"""
191
194
  data = {assembly: {"components": {component: {function: command}}}}
192
195
  _LOGGER.debug(f"Command to send: {data}")
193
196
  if self.use_websockets:
194
197
  _LOGGER.debug("In websocket mode. Sending command over websocket")
195
- data = json.dumps({"type": "command",
198
+ command_data = json.dumps({"type": "command",
196
199
  "request_id": "command",
197
200
  "payload": data
198
201
  })
199
- await self._websocket.send_str(data)
202
+ await self._websocket.send_str(command_data)
200
203
  else:
201
204
  _LOGGER.debug("NOT in websocket mode, sending command over HTTP")
202
205
  async with self._session.post(
@@ -210,7 +213,7 @@ class SwidgetDevice:
210
213
  function_value = state[assembly]["components"][component][function]
211
214
  self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
212
215
 
213
- async def ping(self):
216
+ async def ping(self) -> int | SwidgetException:
214
217
  """Ping the device to ensure it's devices
215
218
 
216
219
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -225,7 +228,7 @@ class SwidgetDevice:
225
228
  except:
226
229
  raise SwidgetException
227
230
 
228
- async def blink(self):
231
+ async def blink(self) -> Any:
229
232
  """Make the device LED blink
230
233
 
231
234
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -240,7 +243,7 @@ class SwidgetDevice:
240
243
  except:
241
244
  raise SwidgetException
242
245
 
243
- async def enable_debug_server(self):
246
+ async def enable_debug_server(self) -> Any:
244
247
  """Enable the Swidget local debug server
245
248
 
246
249
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -255,7 +258,7 @@ class SwidgetDevice:
255
258
  except:
256
259
  raise SwidgetException
257
260
 
258
- async def factory_reset(self):
261
+ async def factory_reset(self) -> Any:
259
262
  """Factory reset the Swidget device
260
263
 
261
264
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -270,7 +273,7 @@ class SwidgetDevice:
270
273
  except:
271
274
  raise SwidgetException
272
275
 
273
- async def check_for_updates(self):
276
+ async def check_for_updates(self) -> Any:
274
277
  """Tell the device to contact the Swidget servers to see if there is an available update
275
278
 
276
279
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -285,7 +288,7 @@ class SwidgetDevice:
285
288
  except:
286
289
  raise SwidgetException
287
290
 
288
- async def update_version(self, version):
291
+ async def update_version(self, version) -> Any:
289
292
  """Tell the device to download and apply an update
290
293
 
291
294
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -299,7 +302,7 @@ class SwidgetDevice:
299
302
  ssl=False,
300
303
  data=json.dumps(data)
301
304
  ) as response:
302
- return await response
305
+ return await response.json()
303
306
  except:
304
307
  raise SwidgetException
305
308
 
@@ -322,7 +325,7 @@ class SwidgetDevice:
322
325
  "rssi": self.rssi
323
326
  }
324
327
 
325
- async def get_child_consumption(self, plug_id=0):
328
+ async def get_child_consumption(self, plug_id=0) -> Any:
326
329
  """Get the power consumption of a plug in watts."""
327
330
  if plug_id == "all":
328
331
  return_dict = {}
@@ -334,7 +337,7 @@ class SwidgetDevice:
334
337
  return return_dict
335
338
  return self.assemblies['host'].components[str(plug_id)].functions['power']['current']
336
339
 
337
- async def total_consumption(self):
340
+ async def total_consumption(self) -> float:
338
341
  """Get the total power consumption in watts."""
339
342
  total_consumption = 0
340
343
  for id, properties in self.assemblies['host'].components.items():
@@ -342,7 +345,7 @@ class SwidgetDevice:
342
345
  return total_consumption
343
346
 
344
347
  @property
345
- async def realtime_values(self):
348
+ async def realtime_values(self) -> Dict:
346
349
  """Get a dict of realtime value attributes from the insert and host
347
350
 
348
351
  :return: A dictionary of insert sensor values and power consumption values
@@ -363,7 +366,7 @@ class SwidgetDevice:
363
366
  try:
364
367
  return list(self.assemblies['host'].components['0'].functions.keys())
365
368
  except KeyError:
366
- return set()
369
+ return list()
367
370
 
368
371
  @property
369
372
  def insert_features(self) -> List[str]:
@@ -371,9 +374,9 @@ class SwidgetDevice:
371
374
  try:
372
375
  return list(self.assemblies['insert'].components.keys())
373
376
  except KeyError:
374
- return set()
377
+ return list()
375
378
 
376
- def get_function_values(self, function: str):
379
+ def get_function_values(self, function: str) -> Dict:
377
380
  """Return the values of an insert function."""
378
381
  return_values = dict()
379
382
  for function, data in self.assemblies['insert'].components[function].functions.items():
@@ -385,7 +388,7 @@ class SwidgetDevice:
385
388
  return_values[function] = data['now']
386
389
  return return_values
387
390
 
388
- def get_sensor_value(self, function, sensor):
391
+ def get_sensor_value(self, function, sensor) -> float | str:
389
392
  """Return the value of a sensor given a function and sensor"""
390
393
  if sensor == "occupied":
391
394
  return self.assemblies['insert'].components[function].functions['occupied']['state']
@@ -395,22 +398,22 @@ class SwidgetDevice:
395
398
  @property
396
399
  def is_outlet(self) -> bool:
397
400
  """Return True if the device is an outlet."""
398
- return self.device_type == "outlet"
401
+ return self.device_type == DeviceType.Outlet
399
402
 
400
403
  @property
401
404
  def is_switch(self) -> bool:
402
405
  """Return True if the device is a switch"""
403
- return self.device_type == "switch" or self.device_type == "pana_switch" or self.device_type == "relay_switch"
406
+ return self.device_type == DeviceType.Switch or self.device_type == DeviceType.TimerSwitch or self.device_type == DeviceType.RelaySwitch
404
407
 
405
408
  @property
406
409
  def is_pana_switch(self) -> bool:
407
410
  """Return True if the device is a pana_switch"""
408
- return self.device_type == "pana_switch"
411
+ return self.device_type == DeviceType.TimerSwitch
409
412
 
410
413
  @property
411
414
  def is_dimmer(self) -> bool:
412
415
  """Return True if the device is a dimmer"""
413
- return self.device_type == "dimmer"
416
+ return self.device_type == DeviceType.Dimmer
414
417
 
415
418
  @property
416
419
  def is_dimmable(self) -> bool:
@@ -430,27 +433,27 @@ class SwidgetDevice:
430
433
  return True
431
434
  return False
432
435
 
433
- async def turn_on(self):
436
+ async def turn_on(self) -> None:
434
437
  """Turn the device on."""
435
438
  _LOGGER.debug("SwidgetDevice.turn_on() called")
436
439
  await self.send_command(
437
440
  assembly="host", component="0", function="toggle", command={"state": "on"}
438
441
  )
439
442
 
440
- async def turn_off(self):
443
+ async def turn_off(self) -> None:
441
444
  """Turn the device off."""
442
445
  _LOGGER.debug("SwidgetDevice.turn_off() called")
443
446
  await self.send_command(
444
447
  assembly="host", component="0", function="toggle", command={"state": "off"}
445
448
  )
446
449
 
447
- async def turn_on_usb_insert(self):
450
+ async def turn_on_usb_insert(self) -> None:
448
451
  """Turn the USB insert on."""
449
452
  await self.send_command(
450
453
  assembly="insert", component="usb", function="toggle", command={"state": "on"}
451
454
  )
452
455
 
453
- async def turn_off_usb_insert(self):
456
+ async def turn_off_usb_insert(self) -> None:
454
457
  """Turn the USB insert off."""
455
458
  await self.send_command(
456
459
  assembly="insert", component="usb", function="toggle", command={"state": "off"}
@@ -464,8 +467,8 @@ class SwidgetDevice:
464
467
  return True
465
468
  return False
466
469
 
467
- def __repr__(self):
468
- if self._last_update is None:
470
+ def __repr__(self) -> str:
471
+ if self._last_update == 0:
469
472
  return f"<{self.device_type} at {self.ip_address} - update() needed>"
470
473
  return f"<{self.device_type} model {self.model} at {self.ip_address}>"
471
474
 
@@ -28,14 +28,14 @@ class SwidgetDimmer(SwidgetDevice):
28
28
  except KeyError:
29
29
  return self.assemblies['host'].components["0"].functions["level"]["default"]
30
30
 
31
- async def set_brightness(self, brightness):
31
+ async def set_brightness(self, brightness) -> None:
32
32
  """Set the brightness of the device."""
33
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
- async def set_default_brightness(self, brightness):
38
+ async def set_default_brightness(self, brightness) -> None:
39
39
  _LOGGER.debug("SwidgetDimmer.set_default_brightness() called")
40
40
  await self.send_command(
41
41
  assembly="host", component="0", function="level", command={"default": brightness}
@@ -8,8 +8,4 @@ class SwidgetSwitch(SwidgetDevice):
8
8
 
9
9
  def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
10
10
  super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
11
- self._device_type = DeviceType.Switch
12
-
13
- async def current_consumption(self) -> float:
14
- """Get the current power consumption in watts."""
15
- return sum([await plug.current_consumption() for plug in self.children])
11
+ self._device_type = DeviceType.Switch
@@ -1,4 +1,6 @@
1
1
  import logging
2
+ from typing import Any
3
+
2
4
  from swidget.swidgetdevice import (
3
5
  DeviceType,
4
6
  )
@@ -13,7 +15,7 @@ class SwidgetTimerSwitch(SwidgetSwitch):
13
15
  super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
14
16
  self._device_type = DeviceType.TimerSwitch
15
17
 
16
- async def set_countdown_timer(self, minutes):
18
+ async def set_countdown_timer(self, minutes) -> Any:
17
19
  """Set the countdown timer."""
18
20
  _LOGGER.debug("SwidgetTimerSwitch.set_brightness() called")
19
21
  await self.send_command(
@@ -1,9 +1,23 @@
1
+ import asyncio
1
2
  import aiohttp
3
+ from aiohttp import ClientWebSocketResponse, WSMsgType
2
4
  import logging
3
5
  import socket
4
6
 
5
7
  _LOGGER = logging.getLogger(__name__)
6
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
+
7
21
  class SwidgetWebsocket:
8
22
  """A websocket connection to a Swidget Device"""
9
23
 
@@ -19,7 +33,7 @@ class SwidgetWebsocket:
19
33
  session=None,
20
34
  use_security=True,
21
35
  ):
22
-
36
+ self.host = host
23
37
  self.session = session or aiohttp.ClientSession()
24
38
  self.use_security = use_security
25
39
  self.uri = self.get_uri(host, token_name, secret_key)
@@ -29,11 +43,17 @@ class SwidgetWebsocket:
29
43
  self.failed_attempts = 0
30
44
  self._error_reason = None
31
45
  self.headers = {'Connection': 'Upgrade'}
46
+ self._receiver_task: asyncio.Task | None = None
32
47
 
33
48
  @property
34
49
  def connected(self) -> bool:
35
50
  return self._client is not None and not self._client.closed
36
51
 
52
+ @property
53
+ def websocket(self) -> ClientWebSocketResponse | None:
54
+ """Return the web socket."""
55
+ return self._client
56
+
37
57
  def get_uri(self, host, token_name, secret_key):
38
58
  """Generate the websocket URI"""
39
59
  if self.use_security:
@@ -43,7 +63,10 @@ class SwidgetWebsocket:
43
63
 
44
64
  async def connect(self) -> None:
45
65
  _LOGGER.debug("websocket.connect() called")
66
+ """Create a new connection and, optionally, start the monitor."""
67
+ await cancel_task(self._receiver_task)
46
68
  if self.connected:
69
+ _LOGGER.debug("Websocket already connected")
47
70
  return
48
71
 
49
72
  if not self.session:
@@ -51,16 +74,20 @@ class SwidgetWebsocket:
51
74
 
52
75
  try:
53
76
  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
- )
77
+ _LOGGER.debug("Websocket now connected")
78
+ except aiohttp.WSServerHandshakeError as handshake_error:
79
+ _LOGGER.error(f"Error occurred during websocket handshake: {handshake_error}")
80
+ raise
81
+ except aiohttp.ClientConnectionError as connection_error:
82
+ _LOGGER.error(f"Error connecting to the websocket server: {connection_error}")
83
+ raise
84
+ except socket.gaierror as gai_error:
85
+ _LOGGER.error(f"Error resolving host: {gai_error}")
86
+ raise
87
+ except Exception as e:
88
+ _LOGGER.error(f"An unexpected error occurred: {e}")
63
89
  raise
90
+ self._receiver_task = asyncio.ensure_future(self.listen())
64
91
 
65
92
  async def close(self) -> None:
66
93
  _LOGGER.debug("websocket.close() called")
@@ -88,7 +115,6 @@ class SwidgetWebsocket:
88
115
 
89
116
  if message.type == aiohttp.WSMsgType.TEXT:
90
117
  message_data = message.json()
91
- _LOGGER.debug(f"Data received from websocket: {message_data}")
92
118
  await self.callback(message_data)
93
119
 
94
120
  if message.type in (
File without changes