python-swidget 1.0.3__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-swidget
3
- Version: 1.0.3
3
+ Version: 1.2.1
4
4
  Summary: Python API for Swidget smart devices
5
5
  Home-page: https://github.com/swidget/python-swidget
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-swidget"
3
- version = "1.0.3"
3
+ version = "1.2.1"
4
4
  description = "Python API for Swidget smart devices"
5
5
  license = "GPL-3.0-or-later"
6
6
  authors = ["Swidget"]
@@ -116,8 +116,8 @@ def join(ssid, network_password, secret_key, friendly_name):
116
116
  confirmation = click.prompt(f"Are you connected to a wifi network that stars with the name 'Swidget-' (y/n)")
117
117
  if confirmation == "y":
118
118
  click.echo(f"Asking the device to connect to network {ssid}..")
119
- # def provision_wifi(ssid, network_password, token_name, secret_key, friendly_name):
120
119
  provision_wifi(friendly_name, ssid, network_password, secret_key)
120
+ click.echo(f"Disconnect from the `swidget` network and connect back your main WiFi network")
121
121
  return True
122
122
  else:
123
123
  click.echo("Not provisioning wifi")
@@ -164,7 +164,7 @@ async def state(dev: SwidgetDevice):
164
164
  click.echo(f"\tMAC (rssi): {dev.mac_address} ({dev.rssi})")
165
165
 
166
166
  click.echo(click.style("\n\t== Current State ==", bold=True))
167
- realtime_values = await dev.realtime_values
167
+ realtime_values = dev.realtime_values
168
168
  for info_name, info_data in realtime_values.items():
169
169
  if isinstance(info_data, list):
170
170
  click.echo(f"\t{info_name}:")
@@ -263,11 +263,11 @@ async def enable_debug_server(dev: SwidgetDevice):
263
263
  async def check_for_updates(dev: SwidgetDevice):
264
264
  click.echo("Contacting Swidget servers to fetch for updates...")
265
265
  available_updates = await dev.check_for_updates()
266
- if len(available_updates['updates']) == 0:
266
+ if len(available_updates) == 0:
267
267
  click.echo("No available updates")
268
268
  else:
269
269
  click.echo("The following versions are available to update to")
270
- for version in available_updates['updates']:
270
+ for version in available_updates:
271
271
  click.echo(click.style(f"\t+ {version}", fg="green"))
272
272
 
273
273
 
@@ -278,7 +278,7 @@ async def upgrade(dev: SwidgetDevice, version: str):
278
278
  if version is None:
279
279
  click.echo("Contacting Swidget servers to fetch for latest version")
280
280
  available_updates = await dev.check_for_updates()
281
- if len(available_updates['updates']) == 0:
281
+ if len(available_updates) == 0:
282
282
  click.echo("No available updates")
283
283
  else:
284
284
  version = available_updates[-1]
@@ -1,9 +1,10 @@
1
1
  import json
2
2
  import logging
3
3
  import time
4
+ from types import TracebackType
4
5
 
5
6
  from aiohttp import ClientSession, TCPConnector
6
- import asyncio
7
+ from collections.abc import Callable
7
8
  from enum import Enum
8
9
  from typing import Any, Dict, List
9
10
 
@@ -23,6 +24,36 @@ class DeviceType(Enum):
23
24
  Unknown = -1
24
25
 
25
26
 
27
+ class InsertType(Enum):
28
+ """Insert type enum."""
29
+ USB = "USB"
30
+ THM = "TEMP HUMI MOTION"
31
+ TH = "TEMP HUMI"
32
+ AQ = "AIR QUALITY"
33
+ GL = "GUIDE LIGHT"
34
+ PO = "POWER OUT"
35
+ Unknown = -1
36
+
37
+
38
+ class SelfDiagnosticErrorCodes(Enum):
39
+ """Self-Diagnostic error codes"""
40
+ UNUSED = 0
41
+ AQ = 1
42
+ GUIDELIGHT = 2
43
+ LIGHT_SENSOR = 3
44
+ MOTION = 4
45
+ POWER_OUT = 5
46
+ PRESSURE = 6
47
+ TEMP = 7
48
+ USB = 8
49
+ VIBRATION = 9
50
+ VIDEO = 10
51
+ ADVANCED_GL = 11
52
+ HUMI = 12
53
+ CO2 = 13
54
+ PART_MATTER = 14
55
+
56
+
26
57
  class SwidgetDevice:
27
58
  def __init__(self, host, token_name, secret_key, use_https=True, use_websockets=True) -> None:
28
59
  self.token_name = token_name
@@ -34,6 +65,7 @@ class SwidgetDevice:
34
65
  self.device_type = DeviceType.Unknown
35
66
  self._friendly_name = "Unknown Swidget Device"
36
67
  self.assemblies: Dict[Any, Any] = dict()
68
+ self._subscribers: List[Any] = list()
37
69
  headers = {self.token_name: self.secret_key,
38
70
  'Connection': 'keep-alive'}
39
71
  connector = TCPConnector(verify_ssl=False, force_close=True)
@@ -47,6 +79,10 @@ class SwidgetDevice:
47
79
  callback=self.message_callback,
48
80
  session=self._session)
49
81
 
82
+ @property
83
+ def connected(self) -> bool:
84
+ return self._websocket.connected
85
+
50
86
  def get_websocket(self) -> SwidgetWebsocket | None:
51
87
  if self.use_websockets:
52
88
  return self._websocket
@@ -77,6 +113,23 @@ class SwidgetDevice:
77
113
  async def close(self) -> None:
78
114
  await self.stop()
79
115
 
116
+ async def disconnect(self) -> None:
117
+ await self.stop()
118
+
119
+ def add_event_callback(self, callback: Callable[[Dict, Any], None],) -> bool:
120
+ for c in self._subscribers:
121
+ if c == callback:
122
+ _LOGGER.warn(f"Callback has already been added, not adding the same callback function again")
123
+ return False
124
+ self._subscribers.append(callback)
125
+ return True
126
+
127
+ def remove_event_callback(self, callback: Callable[[Dict, Any], None],) -> bool:
128
+ if callback in self._subscribers:
129
+ self._subscribers.remove(callback)
130
+ return True
131
+ return False
132
+
80
133
  async def message_callback(self, message) -> None:
81
134
  """Entrypoint for a websocket callback"""
82
135
  _LOGGER.debug("SwidgetDevice.message_callback() called")
@@ -88,6 +141,12 @@ class SwidgetDevice:
88
141
  await self.process_state(message)
89
142
  else:
90
143
  _LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
144
+ await self.signal_callbacks(message)
145
+
146
+ async def signal_callbacks(self, message):
147
+ _LOGGER.debug("SwidgetDevice.signal_callsbacks() called")
148
+ for callback in self._subscribers:
149
+ await callback(message)
91
150
 
92
151
  async def get_summary(self) -> None:
93
152
  """Get a summary of the device over HTTP"""
@@ -213,7 +272,7 @@ class SwidgetDevice:
213
272
  function_value = state[assembly]["components"][component][function]
214
273
  self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
215
274
 
216
- async def ping(self) -> int | SwidgetException:
275
+ async def ping(self) -> bool:
217
276
  """Ping the device to ensure it's devices
218
277
 
219
278
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -224,9 +283,11 @@ class SwidgetDevice:
224
283
  url=f"{self.uri_scheme}://{self.ip_address}/ping",
225
284
  ssl=False
226
285
  ) as response:
227
- return response.status
286
+ if response.status == 200:
287
+ return True
288
+ return False
228
289
  except:
229
- raise SwidgetException
290
+ return False
230
291
 
231
292
  async def blink(self) -> Any:
232
293
  """Make the device LED blink
@@ -254,7 +315,9 @@ class SwidgetDevice:
254
315
  url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
255
316
  ssl=False
256
317
  ) as response:
257
- return await response.json()
318
+ if response.status == 200:
319
+ return True
320
+ return False
258
321
  except:
259
322
  raise SwidgetException
260
323
 
@@ -284,7 +347,8 @@ class SwidgetDevice:
284
347
  url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
285
348
  ssl=False
286
349
  ) as response:
287
- return await response.json()
350
+ newer_versions = await response.json()
351
+ return sorted(newer_versions['updates'])
288
352
  except:
289
353
  raise SwidgetException
290
354
 
@@ -298,11 +362,14 @@ class SwidgetDevice:
298
362
  "version": version
299
363
  }
300
364
  async with self._session.post(
301
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
365
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update/version",
302
366
  ssl=False,
303
367
  data=json.dumps(data)
304
368
  ) as response:
305
- return await response.json()
369
+ result = response.status
370
+ if result == 200:
371
+ return True
372
+ return False
306
373
  except:
307
374
  raise SwidgetException
308
375
 
@@ -325,7 +392,7 @@ class SwidgetDevice:
325
392
  "rssi": self.rssi
326
393
  }
327
394
 
328
- async def get_child_consumption(self, plug_id=0) -> Any:
395
+ def get_child_consumption(self, plug_id=0) -> Any:
329
396
  """Get the power consumption of a plug in watts."""
330
397
  if plug_id == "all":
331
398
  return_dict = {}
@@ -337,7 +404,7 @@ class SwidgetDevice:
337
404
  return return_dict
338
405
  return self.assemblies['host'].components[str(plug_id)].functions['power']['current']
339
406
 
340
- async def total_consumption(self) -> float:
407
+ def total_consumption(self) -> float:
341
408
  """Get the total power consumption in watts."""
342
409
  total_consumption = 0
343
410
  for id, properties in self.assemblies['host'].components.items():
@@ -345,7 +412,7 @@ class SwidgetDevice:
345
412
  return total_consumption
346
413
 
347
414
  @property
348
- async def realtime_values(self) -> Dict:
415
+ def realtime_values(self) -> Dict:
349
416
  """Get a dict of realtime value attributes from the insert and host
350
417
 
351
418
  :return: A dictionary of insert sensor values and power consumption values
@@ -355,7 +422,7 @@ class SwidgetDevice:
355
422
  for feature in self.insert_features:
356
423
  return_dict.update(self.get_function_values(feature))
357
424
  return_dict.update({'rssi': self.rssi})
358
- power_values = await self.get_child_consumption("all")
425
+ power_values =self.get_child_consumption("all")
359
426
  if power_values:
360
427
  return_dict.update(power_values)
361
428
  return return_dict
@@ -428,6 +495,7 @@ class SwidgetDevice:
428
495
  @property # type: ignore
429
496
  def is_on(self) -> bool:
430
497
  """Return whether device is on."""
498
+ _LOGGER.debug("SwidgetDevice.is_on called")
431
499
  dimmer_state = self.assemblies['host'].components["0"].functions['toggle']["state"]
432
500
  if dimmer_state == "on":
433
501
  return True
@@ -436,6 +504,7 @@ class SwidgetDevice:
436
504
  async def turn_on(self) -> None:
437
505
  """Turn the device on."""
438
506
  _LOGGER.debug("SwidgetDevice.turn_on() called")
507
+ self.assemblies['host'].components["0"].functions['toggle']["state"] = "on"
439
508
  await self.send_command(
440
509
  assembly="host", component="0", function="toggle", command={"state": "on"}
441
510
  )
@@ -443,18 +512,23 @@ class SwidgetDevice:
443
512
  async def turn_off(self) -> None:
444
513
  """Turn the device off."""
445
514
  _LOGGER.debug("SwidgetDevice.turn_off() called")
515
+ self.assemblies['host'].components["0"].functions['toggle']["state"] = "off"
446
516
  await self.send_command(
447
517
  assembly="host", component="0", function="toggle", command={"state": "off"}
448
518
  )
449
519
 
450
520
  async def turn_on_usb_insert(self) -> None:
451
521
  """Turn the USB insert on."""
522
+ _LOGGER.debug("SwidgetDevice.turn_on_usb_insert() called")
523
+ self.assemblies['insert'].components["usb"].functions['toggle']["state"] = "on"
452
524
  await self.send_command(
453
525
  assembly="insert", component="usb", function="toggle", command={"state": "on"}
454
526
  )
455
527
 
456
528
  async def turn_off_usb_insert(self) -> None:
457
529
  """Turn the USB insert off."""
530
+ _LOGGER.debug("SwidgetDevice.turn_off_usb_insert() called")
531
+ self.assemblies['insert'].components["usb"].functions['toggle']["state"] = "off"
458
532
  await self.send_command(
459
533
  assembly="insert", component="usb", function="toggle", command={"state": "off"}
460
534
  )
@@ -462,11 +536,23 @@ class SwidgetDevice:
462
536
  @property # type: ignore
463
537
  def usb_is_on(self) -> bool:
464
538
  """Return whether USB is on."""
539
+ _LOGGER.debug("SwidgetDevice.usb_is_on called")
465
540
  usb_state = self.assemblies['insert'].components["usb"].functions['toggle']["state"]
466
541
  if usb_state == "on":
467
542
  return True
468
543
  return False
469
544
 
545
+ async def __aenter__(self) -> "SwidgetDevice":
546
+ """Initialize and connect the Swidget Websocket client."""
547
+ await self.connect()
548
+ return self
549
+
550
+ async def __aexit__(
551
+ self, exc_type: Exception, exc_value: str, traceback: TracebackType
552
+ ) -> None:
553
+ """Disconnect from the websocket."""
554
+ await self.disconnect()
555
+
470
556
  def __repr__(self) -> str:
471
557
  if self._last_update == 0:
472
558
  return f"<{self.device_type} at {self.ip_address} - update() needed>"
@@ -13,7 +13,7 @@ class SwidgetDimmer(SwidgetDevice):
13
13
 
14
14
  def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
15
15
  super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
16
- self._device_type = "dimmer"
16
+ self._device_type = DeviceType.Dimmer
17
17
 
18
18
  @property # type: ignore
19
19
  def brightness(self) -> int:
@@ -21,6 +21,7 @@ class SwidgetDimmer(SwidgetDevice):
21
21
 
22
22
  Will return a range between 0 - 100.
23
23
  """
24
+ _LOGGER.debug("SwidgetDimmer.brightness called")
24
25
  if not self.is_dimmable:
25
26
  raise SwidgetException("Device is not dimmable.")
26
27
  try:
@@ -30,13 +31,14 @@ class SwidgetDimmer(SwidgetDevice):
30
31
 
31
32
  async def set_brightness(self, brightness) -> None:
32
33
  """Set the brightness of the device."""
33
- _LOGGER.debug("SwidgetDimmer.set_brightness() called")
34
+ _LOGGER.debug("SwidgetDimmer.set_brightness() called with brightness: {brightness}")
35
+ self.assemblies['host'].components["0"].functions["level"]["now"] = brightness
34
36
  await self.send_command(
35
37
  assembly="host", component="0", function="level", command={"now": brightness}
36
38
  )
37
39
 
38
40
  async def set_default_brightness(self, brightness) -> None:
39
- _LOGGER.debug("SwidgetDimmer.set_default_brightness() called")
41
+ _LOGGER.debug("SwidgetDimmer.set_default_brightness() called with brightness: {brightness}")
40
42
  await self.send_command(
41
43
  assembly="host", component="0", function="level", command={"default": brightness}
42
44
  )
@@ -44,4 +46,5 @@ class SwidgetDimmer(SwidgetDevice):
44
46
  @property # type: ignore
45
47
  def is_dimmable(self) -> bool:
46
48
  """Whether the switch supports brightness changes."""
49
+ _LOGGER.debug("SwidgetDimmer.is_dimmable() called")
47
50
  return True
@@ -3,6 +3,7 @@ import aiohttp
3
3
  from aiohttp import ClientWebSocketResponse, WSMsgType
4
4
  import logging
5
5
  import socket
6
+ from typing import Any
6
7
 
7
8
  _LOGGER = logging.getLogger(__name__)
8
9
 
@@ -69,10 +70,14 @@ class SwidgetWebsocket:
69
70
  _LOGGER.debug("Websocket already connected")
70
71
  return
71
72
 
73
+ if self._client is not None:
74
+ raise ConnectionError("Already connected")
75
+
72
76
  if not self.session:
73
- raise
77
+ raise ConnectionError("No aiohttp session available")
74
78
 
75
79
  try:
80
+ _LOGGER.debug("Trying to connect")
76
81
  self._client = await self.session.ws_connect(url=self.uri, headers=self.headers, verify_ssl=self._verify_ssl, heartbeat=30)
77
82
  _LOGGER.debug("Websocket now connected")
78
83
  except aiohttp.WSServerHandshakeError as handshake_error:
@@ -91,13 +96,18 @@ class SwidgetWebsocket:
91
96
 
92
97
  async def close(self) -> None:
93
98
  _LOGGER.debug("websocket.close() called")
94
- if not self._client or not self.connected:
95
- return
96
- await self._client.close()
99
+ if self._client is not None and not self._client.closed:
100
+ await self._client.close()
101
+ self._client = None
102
+
103
+ async def disconnect(self) -> None:
104
+ await self.close()
97
105
 
98
106
  async def send_str(self, message):
99
107
  """Send a message through the websocket."""
100
108
  _LOGGER.debug("websocket.send_str() called")
109
+ if not self.connected:
110
+ raise ConnectionError
101
111
  message = str(message)
102
112
  _LOGGER.debug(f"Sending messsage over websocket: {message}")
103
113
  await self._client.send_str(f'{message}')
@@ -115,6 +125,7 @@ class SwidgetWebsocket:
115
125
 
116
126
  if message.type == aiohttp.WSMsgType.TEXT:
117
127
  message_data = message.json()
128
+ _LOGGER.debug(f"Received from websocket: {message_data}")
118
129
  await self.callback(message_data)
119
130
 
120
131
  if message.type in (
@@ -122,4 +133,35 @@ class SwidgetWebsocket:
122
133
  aiohttp.WSMsgType.CLOSED,
123
134
  aiohttp.WSMsgType.CLOSING,
124
135
  ):
125
- _LOGGER.debug("Connection to the Swidget WebSocket on has been closed")
136
+ _LOGGER.error("Connection to the Swidget WebSocket on has been closed")
137
+
138
+ async def receive_message_or_raise(self) -> Any:
139
+ """Receive ONE (raw) message or raise."""
140
+ assert self._client
141
+ ws_msg = await self._client.receive()
142
+
143
+ if ws_msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING):
144
+ raise ConnectionError("Connection was closed.")
145
+
146
+ if ws_msg.type == WSMsgType.ERROR:
147
+ raise ConnectionError
148
+
149
+ if ws_msg.type != WSMsgType.TEXT:
150
+ raise ValueError(
151
+ f"Received non-Text message: {ws_msg.type}: {ws_msg.data}"
152
+ )
153
+
154
+ try:
155
+ msg = ws_msg.json()
156
+ except TypeError as err:
157
+ raise TypeError(f"Received unsupported JSON: {err}") from err
158
+ except ValueError as err:
159
+ raise ValueError("Received invalid JSON.") from err
160
+
161
+ _LOGGER.debug(f"Received message:\n{msg}\n")
162
+ return msg
163
+
164
+ def __repr__(self) -> str:
165
+ """Return the representation."""
166
+ prefix = "" if self.connected else "not "
167
+ return f"{type(self).__name__}(ws_server_url={self.host}, {prefix}connected)"
File without changes