python-swidget 1.0.3__tar.gz → 1.2.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: 1.0.3
3
+ Version: 1.2.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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-swidget"
3
- version = "1.0.3"
3
+ version = "1.2.0"
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: Callable[[Dict[str, Any]], 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,20 @@ class SwidgetDevice:
77
113
  async def close(self) -> None:
78
114
  await self.stop()
79
115
 
116
+ async def add_event_callback(self, callback: Callable[[Dict, Any], None],) -> bool:
117
+ for c in self._subscribers:
118
+ if c == callback:
119
+ _LOGGER.warn(f"Callback has already been added, not adding the same callback function again")
120
+ return False
121
+ self._subscribers.append(callback)
122
+ return True
123
+
124
+ async def remove_event_callback(self, callback: Callable[[Dict, Any], None],) -> bool:
125
+ if callback in self._subscribers:
126
+ self._subscribers.remove(callback)
127
+ return True
128
+ return False
129
+
80
130
  async def message_callback(self, message) -> None:
81
131
  """Entrypoint for a websocket callback"""
82
132
  _LOGGER.debug("SwidgetDevice.message_callback() called")
@@ -88,6 +138,12 @@ class SwidgetDevice:
88
138
  await self.process_state(message)
89
139
  else:
90
140
  _LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
141
+ await self.signal_callbacks(message)
142
+
143
+ async def signal_callbacks(self, message):
144
+ _LOGGER.debug("SwidgetDevice.signal_callsbacks() called")
145
+ for callback in self._subscribers:
146
+ await callback(message)
91
147
 
92
148
  async def get_summary(self) -> None:
93
149
  """Get a summary of the device over HTTP"""
@@ -213,7 +269,7 @@ class SwidgetDevice:
213
269
  function_value = state[assembly]["components"][component][function]
214
270
  self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
215
271
 
216
- async def ping(self) -> int | SwidgetException:
272
+ async def ping(self) -> bool:
217
273
  """Ping the device to ensure it's devices
218
274
 
219
275
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
@@ -224,9 +280,11 @@ class SwidgetDevice:
224
280
  url=f"{self.uri_scheme}://{self.ip_address}/ping",
225
281
  ssl=False
226
282
  ) as response:
227
- return response.status
283
+ if response.status == 200:
284
+ return True
285
+ return False
228
286
  except:
229
- raise SwidgetException
287
+ return False
230
288
 
231
289
  async def blink(self) -> Any:
232
290
  """Make the device LED blink
@@ -254,7 +312,9 @@ class SwidgetDevice:
254
312
  url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
255
313
  ssl=False
256
314
  ) as response:
257
- return await response.json()
315
+ if response.status == 200:
316
+ return True
317
+ return False
258
318
  except:
259
319
  raise SwidgetException
260
320
 
@@ -284,7 +344,8 @@ class SwidgetDevice:
284
344
  url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
285
345
  ssl=False
286
346
  ) as response:
287
- return await response.json()
347
+ newer_versions = await response.json()
348
+ return sorted(newer_versions['updates'])
288
349
  except:
289
350
  raise SwidgetException
290
351
 
@@ -298,11 +359,14 @@ class SwidgetDevice:
298
359
  "version": version
299
360
  }
300
361
  async with self._session.post(
301
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
362
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update/version",
302
363
  ssl=False,
303
364
  data=json.dumps(data)
304
365
  ) as response:
305
- return await response.json()
366
+ result = await response.status
367
+ if result == 200:
368
+ return True
369
+ return False
306
370
  except:
307
371
  raise SwidgetException
308
372
 
@@ -428,6 +492,7 @@ class SwidgetDevice:
428
492
  @property # type: ignore
429
493
  def is_on(self) -> bool:
430
494
  """Return whether device is on."""
495
+ _LOGGER.debug("SwidgetDevice.is_on called")
431
496
  dimmer_state = self.assemblies['host'].components["0"].functions['toggle']["state"]
432
497
  if dimmer_state == "on":
433
498
  return True
@@ -436,6 +501,7 @@ class SwidgetDevice:
436
501
  async def turn_on(self) -> None:
437
502
  """Turn the device on."""
438
503
  _LOGGER.debug("SwidgetDevice.turn_on() called")
504
+ self.assemblies['host'].components["0"].functions['toggle']["state"] = "on"
439
505
  await self.send_command(
440
506
  assembly="host", component="0", function="toggle", command={"state": "on"}
441
507
  )
@@ -443,18 +509,23 @@ class SwidgetDevice:
443
509
  async def turn_off(self) -> None:
444
510
  """Turn the device off."""
445
511
  _LOGGER.debug("SwidgetDevice.turn_off() called")
512
+ self.assemblies['host'].components["0"].functions['toggle']["state"] = "off"
446
513
  await self.send_command(
447
514
  assembly="host", component="0", function="toggle", command={"state": "off"}
448
515
  )
449
516
 
450
517
  async def turn_on_usb_insert(self) -> None:
451
518
  """Turn the USB insert on."""
519
+ _LOGGER.debug("SwidgetDevice.turn_on_usb_insert() called")
520
+ self.assemblies['insert'].components["usb"].functions['toggle']["state"] = "on"
452
521
  await self.send_command(
453
522
  assembly="insert", component="usb", function="toggle", command={"state": "on"}
454
523
  )
455
524
 
456
525
  async def turn_off_usb_insert(self) -> None:
457
526
  """Turn the USB insert off."""
527
+ _LOGGER.debug("SwidgetDevice.turn_off_usb_insert() called")
528
+ self.assemblies['insert'].components["usb"].functions['toggle']["state"] = "off"
458
529
  await self.send_command(
459
530
  assembly="insert", component="usb", function="toggle", command={"state": "off"}
460
531
  )
@@ -462,11 +533,29 @@ class SwidgetDevice:
462
533
  @property # type: ignore
463
534
  def usb_is_on(self) -> bool:
464
535
  """Return whether USB is on."""
536
+ _LOGGER.debug("SwidgetDevice.usb_is_on called")
465
537
  usb_state = self.assemblies['insert'].components["usb"].functions['toggle']["state"]
466
538
  if usb_state == "on":
467
539
  return True
468
540
  return False
469
541
 
542
+ async def __aenter__(self) -> "SwidgetDevice":
543
+ """Initialize and connect the Swidget Websocket client."""
544
+ await self.connect()
545
+ return self
546
+
547
+ async def __aexit__(
548
+ self, exc_type: Exception, exc_value: str, traceback: TracebackType
549
+ ) -> None:
550
+ """Disconnect from the websocket."""
551
+ await self.disconnect()
552
+
553
+ def __repr__(self) -> str:
554
+ """Return the representation."""
555
+ url = self.connection.ws_server_url
556
+ prefix = "" if self.connection.connected else "not "
557
+ return f"{type(self).__name__}(ws_server_url={url!r}, {prefix}connected)"
558
+
470
559
  def __repr__(self) -> str:
471
560
  if self._last_update == 0:
472
561
  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._ws_client
141
+ ws_msg = await self._ws_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