python-swidget 1.2.2__py3-none-any.whl → 1.2.4__py3-none-any.whl

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.
swidget/swidgetdevice.py CHANGED
@@ -1,18 +1,21 @@
1
+ """Module (main-class) that represents a Swidget device."""
2
+
1
3
  import json
2
4
  import logging
3
5
  import time
4
- from types import TracebackType
5
-
6
- from aiohttp import ClientSession, TCPConnector
7
6
  from collections.abc import Callable
8
7
  from enum import Enum
8
+ from types import TracebackType
9
9
  from typing import Any, Dict, List
10
10
 
11
+ from aiohttp import ClientSession, TCPConnector
12
+
11
13
  from .exceptions import SwidgetException
12
14
  from .websocket import SwidgetWebsocket
13
15
 
14
16
  _LOGGER = logging.getLogger(__name__)
15
17
 
18
+
16
19
  class DeviceType(Enum):
17
20
  """Device type enum."""
18
21
 
@@ -26,17 +29,20 @@ class DeviceType(Enum):
26
29
 
27
30
  class InsertType(Enum):
28
31
  """Insert type enum."""
32
+
29
33
  USB = "USB"
30
34
  THM = "TEMP HUMI MOTION"
31
35
  TH = "TEMP HUMI"
32
36
  AQ = "AIR QUALITY"
33
37
  GL = "GUIDE LIGHT"
34
38
  PO = "POWER OUT"
39
+ VIDEO = "video" # This is not a mistake.
35
40
  Unknown = -1
36
41
 
37
42
 
38
43
  class SelfDiagnosticErrorCodes(Enum):
39
- """Self-Diagnostic error codes"""
44
+ """Self-Diagnostic error codes."""
45
+
40
46
  UNUSED = 0
41
47
  AQ = 1
42
48
  GUIDELIGHT = 2
@@ -55,21 +61,28 @@ class SelfDiagnosticErrorCodes(Enum):
55
61
 
56
62
 
57
63
  class SwidgetDevice:
58
- def __init__(self, host, token_name, secret_key, use_https=True, use_websockets=True) -> None:
64
+ """Class to represent the Swidge device."""
65
+
66
+ def __init__(
67
+ self, host, token_name, secret_key, use_https=True, use_websockets=True
68
+ ) -> None:
59
69
  self.token_name = token_name
60
70
  self.ip_address = host
61
71
  self.use_https = use_https
62
- self.uri_scheme = 'https' if self.use_https is True else 'http'
72
+ self.uri_scheme = "https" if self.use_https is True else "http"
63
73
  self.secret_key = secret_key
64
74
  self.use_websockets = use_websockets
65
75
  self.device_type = DeviceType.Unknown
66
76
  self._friendly_name = "Unknown Swidget Device"
67
77
  self.assemblies: Dict[Any, Any] = dict()
78
+ self.device_config: DeviceConfiguration = DeviceConfiguration({})
68
79
  self._subscribers: List[Any] = list()
69
- headers = {self.token_name: self.secret_key,
70
- 'Connection': 'keep-alive'}
80
+ headers = {self.token_name: self.secret_key, "Connection": "keep-alive"}
71
81
  connector = TCPConnector(verify_ssl=False, force_close=True)
72
- self._session = ClientSession(headers=headers, connector=connector)
82
+ if use_https is True:
83
+ self._session = ClientSession(headers=headers, connector=connector)
84
+ else:
85
+ self._session = ClientSession(connector=connector)
73
86
  self._last_update: int = 0
74
87
  if self.use_websockets:
75
88
  self._websocket = SwidgetWebsocket(
@@ -77,21 +90,26 @@ class SwidgetDevice:
77
90
  token_name=self.token_name,
78
91
  secret_key=self.secret_key,
79
92
  callback=self.message_callback,
80
- session=self._session)
93
+ session=self._session,
94
+ )
81
95
 
82
96
  @property
83
97
  def connected(self) -> bool:
98
+ """Property to represent if the client is connected to the device."""
84
99
  return self._websocket.connected
85
100
 
86
101
  def get_websocket(self) -> SwidgetWebsocket | None:
102
+ """Return the SwidgetWebsocket class instance if possible."""
87
103
  if self.use_websockets:
88
104
  return self._websocket
89
105
  return None
90
106
 
91
107
  def set_countdown_timer(self, minutes) -> Any:
108
+ """Set the countdown timer."""
92
109
  raise NotImplementedError()
93
110
 
94
111
  async def connect(self) -> None:
112
+ """Create a new connection to the device."""
95
113
  await self._websocket.connect()
96
114
 
97
115
  async def start(self) -> None:
@@ -103,57 +121,100 @@ class SwidgetDevice:
103
121
  _LOGGER.debug("Calling self.update() ")
104
122
  await self.update()
105
123
 
106
- async def stop(self) -> None:
124
+ async def stop(self) -> bool:
107
125
  """Stop the websocket."""
108
126
  _LOGGER.debug("SwidgetDevice.stop()")
109
- if hasattr(self, '_websocket'):
110
- await self._websocket.close()
111
- await self._session.close()
127
+ if hasattr(self, "_websocket"):
128
+ try:
129
+ await self._websocket.close()
130
+ except Exception:
131
+ return False
132
+ try:
133
+ await self._session.close()
134
+ return True
135
+ except Exception:
136
+ return False
112
137
 
113
138
  async def close(self) -> None:
139
+ """Wrapper for the stop() function."""
114
140
  await self.stop()
115
141
 
116
142
  async def disconnect(self) -> None:
143
+ """Wrapper for the stop() function."""
117
144
  await self.stop()
118
145
 
119
146
  def add_event_callback(self, callback: Callable[[Any], Any]):
147
+ """Register a function to be called when a new websocket message is recieved."""
120
148
  for c in self._subscribers:
121
149
  if c == callback:
122
- _LOGGER.warn(f"Callback has already been added, not adding the same callback function again")
150
+ _LOGGER.warn(
151
+ "Callback has already been added, not adding the same callback function again"
152
+ )
123
153
  return False
124
154
  self._subscribers.append(callback)
125
155
  return True
126
156
 
127
157
  def remove_event_callback(self, callback: Callable[[Any], Any]):
158
+ """Remove a registered callback function."""
128
159
  if callback in self._subscribers:
129
160
  self._subscribers.remove(callback)
130
161
  return True
131
162
  return False
132
163
 
133
164
  async def message_callback(self, message) -> None:
134
- """Entrypoint for a websocket callback"""
165
+ """Entrypoint for a websocket callback."""
135
166
  _LOGGER.debug("SwidgetDevice.message_callback() called")
136
167
  if message["request_id"] == "summary":
137
168
  _LOGGER.debug("Calling SwidgetDevice.process_summary()")
138
169
  await self.process_summary(message)
139
- elif message["request_id"] == "state" or message["request_id"] == "DYNAMIC_UPDATE" or message["request_id"] == "command":
170
+ elif (
171
+ message["request_id"] == "state"
172
+ or message["request_id"] == "DYNAMIC_UPDATE"
173
+ or message["request_id"] == "command"
174
+ ):
140
175
  _LOGGER.debug("Calling SwidgetDevice.process_state()")
141
176
  await self.process_state(message)
142
177
  else:
143
- _LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
178
+ message_type = ["request_id"]
179
+ _LOGGER.error(
180
+ f"Unknown message type from websocket. Type given was: {message_type}"
181
+ )
144
182
  await self.signal_callbacks(message)
145
183
 
146
184
  async def signal_callbacks(self, message):
185
+ """Call any available registered callback functions."""
147
186
  _LOGGER.debug("SwidgetDevice.signal_callsbacks() called")
148
187
  for callback in self._subscribers:
149
188
  await callback(message)
150
189
 
190
+ async def get_device_config(self) -> Any:
191
+ """Get the config of the device."""
192
+ _LOGGER.debug("SwidgetDevice.get_device_config() called")
193
+ if self.use_websockets:
194
+ _LOGGER.debug(
195
+ "In websocket mode. Sending get_summary() command over websocket"
196
+ )
197
+ raise NotImplementedError
198
+ else:
199
+ _LOGGER.debug("In http mode. Sending get_summary() command over http")
200
+ async with self._session.get(
201
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/device_config",
202
+ ssl=False,
203
+ ) as response:
204
+ config = await response.json()
205
+ self.device_config = DeviceConfiguration(config)
206
+ self._last_update = int(time.time())
207
+
151
208
  async def get_summary(self) -> None:
152
- """Get a summary of the device over HTTP"""
209
+ """Get a summary of the device over HTTP."""
153
210
  _LOGGER.debug("SwidgetDevice.get_summary() called")
154
211
  if self.use_websockets:
155
- _LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
156
- await self._websocket.send_str(json.dumps({"type": "summary", "request_id": "summary"}))
212
+ _LOGGER.debug(
213
+ "In websocket mode. Sending get_summary() command over websocket"
214
+ )
215
+ await self._websocket.send_str(
216
+ json.dumps({"type": "summary", "request_id": "summary"})
217
+ )
157
218
  else:
158
219
  _LOGGER.debug("In http mode. Sending get_summary() command over http")
159
220
  async with self._session.get(
@@ -163,7 +224,7 @@ class SwidgetDevice:
163
224
  await self.process_summary(summary)
164
225
 
165
226
  async def process_summary(self, summary) -> None:
166
- """ Process the data around the summary of the device"""
227
+ """Process the data around the summary of the device."""
167
228
  _LOGGER.debug("SwidgetDevice.process_summary() called")
168
229
  _LOGGER.debug(f"Summary to process: {summary}")
169
230
  self.model = summary["model"]
@@ -173,12 +234,13 @@ class SwidgetDevice:
173
234
  "host": SwidgetAssembly(summary["host"]),
174
235
  "insert": SwidgetAssembly(summary["insert"]),
175
236
  }
176
- self.device_type = DeviceType(self.assemblies['host'].type)
177
- self.insert_type = self.assemblies['insert'].type
178
- self.id = self.assemblies['host'].id
237
+ self.device_type = DeviceType(self.assemblies["host"].type)
238
+ self.insert_type = InsertType(self.assemblies["insert"].type)
239
+ self.id = self.assemblies["host"].id
179
240
  self._last_update = int(time.time())
180
241
 
181
242
  async def get_friendly_name(self) -> None:
243
+ """Retrieve the friendly name of the device."""
182
244
  _LOGGER.debug("SwidgetDevice.get_friendly_name() called")
183
245
  try:
184
246
  async with self._session.get(
@@ -187,19 +249,24 @@ class SwidgetDevice:
187
249
  name = await response.json()
188
250
  except Exception:
189
251
  name = {"name": f"Swidget {self.device_type} w/{self.insert_type} insert"}
190
- await self.process_friendly_name(name['name'])
252
+ await self.process_friendly_name(name["name"])
191
253
 
192
254
  async def process_friendly_name(self, name) -> None:
255
+ """Process the data retrieved from get_friendly_name() an set the device name."""
193
256
  _LOGGER.debug("SwidgetDevice.process_friendly_name() called")
194
257
  self._friendly_name = name
195
258
  self._last_update = int(time.time())
196
259
 
197
260
  async def get_state(self) -> None:
198
- """ Get the state of the device over HTTP"""
261
+ """Get the state of the device over HTTP."""
199
262
  _LOGGER.debug("SwidgetDevice.get_state() called")
200
263
  if self.use_websockets:
201
- _LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
202
- await self._websocket.send_str(json.dumps({"type": "state", "request_id": "state"}))
264
+ _LOGGER.debug(
265
+ "In websocket mode. Sending get_summary() command over websocket"
266
+ )
267
+ await self._websocket.send_str(
268
+ json.dumps({"type": "state", "request_id": "state"})
269
+ )
203
270
  else:
204
271
  _LOGGER.debug("In http mode. Sending get_summary() command over http")
205
272
  async with self._session.get(
@@ -209,23 +276,24 @@ class SwidgetDevice:
209
276
  await self.process_state(state)
210
277
 
211
278
  async def process_state(self, state) -> None:
212
- """ Process any information about the state of the device or insert"""
279
+ """Process any information about the state of the device or insert."""
213
280
  # State is not always in the state (during callback)
214
281
  _LOGGER.debug("SwidgetDevice.process_state() called")
215
282
  _LOGGER.debug(f"State to process: {state}")
216
283
  try:
217
284
  self.rssi = state["connection"]["rssi"]
218
- except:
285
+ except Exception:
219
286
  pass
220
287
  for assembly in self.assemblies:
221
288
  for id, component in self.assemblies[assembly].components.items():
222
289
  try:
223
290
  component.functions.update(state[assembly]["components"][id])
224
- except:
291
+ except Exception:
225
292
  pass
226
293
  self._last_update = int(time.time())
227
294
 
228
295
  async def update(self) -> None:
296
+ """Update the state, summary, config and name of the device."""
229
297
  _LOGGER.debug("SwidgetDevice.update() called")
230
298
  if self._last_update == 0:
231
299
  _LOGGER.debug("Performing the initial update to obtain sysinfo")
@@ -233,6 +301,8 @@ class SwidgetDevice:
233
301
  await self.get_state()
234
302
  if self._friendly_name == "Unknown Swidget Device":
235
303
  await self.get_friendly_name()
304
+ if self.device_config == {}:
305
+ await self.get_device_config()
236
306
  elif (int(time.time()) - self._last_update) < 5:
237
307
  _LOGGER.debug("update() recently called, not executing")
238
308
  else:
@@ -241,23 +311,25 @@ class SwidgetDevice:
241
311
  await self.get_state()
242
312
 
243
313
  async def send_config(self, payload: dict) -> None:
314
+ """Send a config block to the device."""
244
315
  _LOGGER.debug("SwidgetDevice.send_config() called")
245
- data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
316
+ data = json.dumps(
317
+ {"type": "config", "request_id": "send_config", "payload": payload}
318
+ )
246
319
  await self._websocket.send_str(data)
247
320
 
248
321
  async def send_command(
249
322
  self, assembly: str, component: str, function: str, command: dict
250
323
  ) -> None:
324
+ """Send a command to the Swidget device either using a HTTP call or the existing websocket."""
251
325
  _LOGGER.debug("SwidgetDevice.send_command() called")
252
- """Send a command to the Swidget device either using a HTTP call or the existing websocket"""
253
326
  data = {assembly: {"components": {component: {function: command}}}}
254
327
  _LOGGER.debug(f"Command to send: {data}")
255
328
  if self.use_websockets:
256
329
  _LOGGER.debug("In websocket mode. Sending command over websocket")
257
- command_data = json.dumps({"type": "command",
258
- "request_id": "command",
259
- "payload": data
260
- })
330
+ command_data = json.dumps(
331
+ {"type": "command", "request_id": "command", "payload": data}
332
+ )
261
333
  await self._websocket.send_str(command_data)
262
334
  else:
263
335
  _LOGGER.debug("NOT in websocket mode, sending command over HTTP")
@@ -273,39 +345,37 @@ class SwidgetDevice:
273
345
  self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
274
346
 
275
347
  async def ping(self) -> bool:
276
- """Ping the device to ensure it's devices
348
+ """Ping the device to ensure it's devices.
277
349
 
278
350
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
279
351
  """
280
352
  _LOGGER.debug("SwidgetDevice.ping() called")
281
353
  try:
282
354
  async with self._session.get(
283
- url=f"{self.uri_scheme}://{self.ip_address}/ping",
284
- ssl=False
355
+ url=f"{self.uri_scheme}://{self.ip_address}/ping", ssl=False
285
356
  ) as response:
286
357
  if response.status == 200:
287
358
  return True
288
359
  return False
289
- except:
360
+ except Exception:
290
361
  return False
291
362
 
292
363
  async def blink(self) -> Any:
293
- """Make the device LED blink
364
+ """Make the device LED blink.
294
365
 
295
366
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
296
367
  """
297
368
  _LOGGER.debug("SwidgetDevice.blink() called")
298
369
  try:
299
370
  async with self._session.get(
300
- url=f"{self.uri_scheme}://{self.ip_address}/blink",
301
- ssl=False
371
+ url=f"{self.uri_scheme}://{self.ip_address}/blink", ssl=False
302
372
  ) as response:
303
373
  return await response.json()
304
- except:
374
+ except Exception:
305
375
  raise SwidgetException
306
376
 
307
377
  async def enable_debug_server(self) -> Any:
308
- """Enable the Swidget local debug server
378
+ """Enable the Swidget local debug server.
309
379
 
310
380
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
311
381
  """
@@ -313,64 +383,74 @@ class SwidgetDevice:
313
383
  try:
314
384
  async with self._session.get(
315
385
  url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
316
- ssl=False
386
+ ssl=False,
317
387
  ) as response:
318
388
  if response.status == 200:
319
389
  return True
320
390
  return False
321
- except:
391
+ except Exception:
392
+ raise SwidgetException
393
+
394
+ async def restart_device(self) -> Any:
395
+ """Restart the Swidget device.
396
+
397
+ :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
398
+ """
399
+ try:
400
+
401
+ async with self._session.post(
402
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset", ssl=False
403
+ ) as response:
404
+ return await response.json()
405
+ except Exception:
322
406
  raise SwidgetException
323
407
 
324
408
  async def factory_reset(self) -> Any:
325
- """Factory reset the Swidget device
409
+ """Factory reset the Swidget device.
326
410
 
327
411
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
328
412
  """
329
413
  try:
330
414
 
331
415
  async with self._session.delete(
332
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset",
333
- ssl=False
416
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset", ssl=False
334
417
  ) as response:
335
418
  return await response.json()
336
- except:
419
+ except Exception:
337
420
  raise SwidgetException
338
421
 
339
422
  async def check_for_updates(self) -> Any:
340
- """Tell the device to contact the Swidget servers to see if there is an available update
423
+ """Tell the device to contact the Swidget servers to see if there is an available update.
341
424
 
342
425
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
343
426
  """
344
427
  try:
345
428
 
346
429
  async with self._session.get(
347
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
348
- ssl=False
430
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update", ssl=False
349
431
  ) as response:
350
432
  newer_versions = await response.json()
351
- return sorted(newer_versions['updates'])
352
- except:
433
+ return sorted(newer_versions["updates"])
434
+ except Exception:
353
435
  raise SwidgetException
354
436
 
355
437
  async def update_version(self, version) -> Any:
356
- """Tell the device to download and apply an update
438
+ """Tell the device to download and apply an update.
357
439
 
358
440
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
359
441
  """
360
442
  try:
361
- data = {
362
- "version": version
363
- }
443
+ data = {"version": version}
364
444
  async with self._session.post(
365
445
  url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update/version",
366
446
  ssl=False,
367
- data=json.dumps(data)
447
+ data=json.dumps(data),
368
448
  ) as response:
369
449
  result = response.status
370
450
  if result == 200:
371
451
  return True
372
452
  return False
373
- except:
453
+ except Exception:
374
454
  raise SwidgetException
375
455
 
376
456
  @property
@@ -389,31 +469,37 @@ class SwidgetDevice:
389
469
  "insert_type": self.insert_type,
390
470
  "insert_features": self.insert_features,
391
471
  "host_features": self.host_features,
392
- "rssi": self.rssi
472
+ "rssi": self.rssi,
393
473
  }
394
474
 
395
475
  def get_child_consumption(self, plug_id=0) -> Any:
396
476
  """Get the power consumption of a plug in watts."""
397
477
  if plug_id == "all":
398
478
  return_dict = {}
399
- for id, properties in self.assemblies['host'].components.items():
479
+ for id, properties in self.assemblies["host"].components.items():
400
480
  try:
401
- return_dict[f"power_{id}"] = properties.functions['power']['current']
402
- except KeyError: # Hits this when there is no power metering
481
+ return_dict[f"power_{id}"] = properties.functions["power"][
482
+ "current"
483
+ ]
484
+ except KeyError: # Hits this when there is no power metering
403
485
  return None
404
486
  return return_dict
405
- return self.assemblies['host'].components[str(plug_id)].functions['power']['current']
487
+ return (
488
+ self.assemblies["host"]
489
+ .components[str(plug_id)]
490
+ .functions["power"]["current"]
491
+ )
406
492
 
407
493
  def total_consumption(self) -> float:
408
494
  """Get the total power consumption in watts."""
409
495
  total_consumption = 0
410
- for id, properties in self.assemblies['host'].components.items():
411
- total_consumption += properties.functions['power']['current']
496
+ for id, properties in self.assemblies["host"].components.items():
497
+ total_consumption += properties.functions["power"]["current"]
412
498
  return total_consumption
413
499
 
414
500
  @property
415
501
  def realtime_values(self) -> Dict:
416
- """Get a dict of realtime value attributes from the insert and host
502
+ """Get a dict of realtime value attributes from the insert and host.
417
503
 
418
504
  :return: A dictionary of insert sensor values and power consumption values
419
505
  :rtype: dict
@@ -421,8 +507,8 @@ class SwidgetDevice:
421
507
  return_dict = {}
422
508
  for feature in self.insert_features:
423
509
  return_dict.update(self.get_function_values(feature))
424
- return_dict.update({'rssi': self.rssi})
425
- power_values =self.get_child_consumption("all")
510
+ return_dict.update({"rssi": self.rssi})
511
+ power_values = self.get_child_consumption("all")
426
512
  if power_values:
427
513
  return_dict.update(power_values)
428
514
  return return_dict
@@ -431,7 +517,7 @@ class SwidgetDevice:
431
517
  def host_features(self) -> List[str]:
432
518
  """Return a set of features that the host supports."""
433
519
  try:
434
- return list(self.assemblies['host'].components['0'].functions.keys())
520
+ return list(self.assemblies["host"].components["0"].functions.keys())
435
521
  except KeyError:
436
522
  return list()
437
523
 
@@ -439,28 +525,104 @@ class SwidgetDevice:
439
525
  def insert_features(self) -> List[str]:
440
526
  """Return a set of features that the insert supports."""
441
527
  try:
442
- return list(self.assemblies['insert'].components.keys())
528
+ return list(self.assemblies["insert"].components.keys())
443
529
  except KeyError:
444
530
  return list()
445
531
 
446
532
  def get_function_values(self, function: str) -> Dict:
447
533
  """Return the values of an insert function."""
448
534
  return_values = dict()
449
- for function, data in self.assemblies['insert'].components[function].functions.items():
535
+ for function, data in (
536
+ self.assemblies["insert"].components[function].functions.items()
537
+ ):
450
538
  if function == "occupied":
451
- return_values[function] = data['state']
539
+ return_values[function] = data["state"]
452
540
  elif function == "toggle":
453
541
  pass
542
+ elif function == "pic" or function == "audio":
543
+ pass
544
+ elif function == "webrtc":
545
+ return_values["webrtc_max_viewers"] = data["maxViewers"]
546
+ return_values["webrtc_current_viewers"] = data["currentViewers"]
547
+ elif function == "rtsp":
548
+ pass
549
+ elif function == "storage":
550
+ pass
551
+ elif function == "sd":
552
+ return_values[function] = data["state"]
454
553
  else:
455
- return_values[function] = data['now']
554
+ return_values[function] = data["now"]
456
555
  return return_values
457
556
 
458
557
  def get_sensor_value(self, function, sensor) -> float | str:
459
- """Return the value of a sensor given a function and sensor"""
558
+ """Return the value of a sensor given a function and sensor."""
460
559
  if sensor == "occupied":
461
- return self.assemblies['insert'].components[function].functions['occupied']['state']
560
+ return (
561
+ self.assemblies["insert"]
562
+ .components[function]
563
+ .functions["occupied"]["state"]
564
+ )
462
565
  else:
463
- return self.assemblies['insert'].components[function].functions[sensor]['now']
566
+ return (
567
+ self.assemblies["insert"].components[function].functions[sensor]["now"]
568
+ )
569
+
570
+ @property
571
+ def available_streams_types(self) -> List[str]:
572
+ """Return a list of the available stream device types the device supports."""
573
+ stream_types = list()
574
+ """Returns the available RTSP streams."""
575
+ if InsertType.VIDEO.value in self.insert_features:
576
+ for stream_type in (
577
+ self.assemblies["insert"]
578
+ .components["video"]
579
+ .functions["rtsp"]["streams"]
580
+ .values()
581
+ ):
582
+ stream_types.append(stream_type)
583
+ return stream_types
584
+
585
+ @property
586
+ def stream_url(self, encoding: str = "ph264") -> str | NotImplementedError:
587
+ """Returns the RTSP stream URL."""
588
+ if InsertType.VIDEO.value in self.insert_features:
589
+ return f"rtsp://{self.ip_address}:8554/{encoding}"
590
+ raise NotImplementedError
591
+
592
+ @property
593
+ def snapshot_url(self) -> str | NotImplementedError:
594
+ """Returns the URL to take a snapshot."""
595
+ if InsertType.VIDEO.value in self.insert_features:
596
+ return f"http://{self.ip_address}/api/v1/picture"
597
+ raise NotImplementedError
598
+
599
+ async def get_snapshot_bytes(
600
+ self, width: int | None = None, height: int | None = None
601
+ ) -> bytes | None:
602
+ """Returns a 640x360 jpeg or None if the snapshot could not be retrieved.
603
+
604
+ :return: The image bytes or None if the snapshot could not be retrieved.
605
+ :raises: NotImplementedError if the device does not support video.
606
+ """
607
+ _LOGGER.debug("SwidgetDevice.take_snapshot() called")
608
+ if InsertType.VIDEO.value in self.insert_features:
609
+ try:
610
+ snapshot_url = self.snapshot_url
611
+ headers = self._session.headers.copy()
612
+ if width is not None and height is not None:
613
+ headers.update(
614
+ {"x-picture-width": str(width), "x-picture-height": str(height)}
615
+ )
616
+ if isinstance(snapshot_url, str):
617
+ async with self._session.get(
618
+ url=snapshot_url, headers=headers, ssl=False
619
+ ) as response:
620
+ if response.status == 200:
621
+ return await response.read()
622
+ except Exception as e:
623
+ _LOGGER.error(f"Error fetching snapshot: {e}")
624
+ return None
625
+ raise NotImplementedError
464
626
 
465
627
  @property
466
628
  def is_outlet(self) -> bool:
@@ -469,17 +631,21 @@ class SwidgetDevice:
469
631
 
470
632
  @property
471
633
  def is_switch(self) -> bool:
472
- """Return True if the device is a switch"""
473
- return self.device_type == DeviceType.Switch or self.device_type == DeviceType.TimerSwitch or self.device_type == DeviceType.RelaySwitch
634
+ """Return True if the device is a switch."""
635
+ return (
636
+ self.device_type == DeviceType.Switch
637
+ or self.device_type == DeviceType.TimerSwitch
638
+ or self.device_type == DeviceType.RelaySwitch
639
+ )
474
640
 
475
641
  @property
476
642
  def is_pana_switch(self) -> bool:
477
- """Return True if the device is a pana_switch"""
643
+ """Return True if the device is a pana_switch."""
478
644
  return self.device_type == DeviceType.TimerSwitch
479
645
 
480
646
  @property
481
647
  def is_dimmer(self) -> bool:
482
- """Return True if the device is a dimmer"""
648
+ """Return True if the device is a dimmer."""
483
649
  return self.device_type == DeviceType.Dimmer
484
650
 
485
651
  @property
@@ -489,14 +655,16 @@ class SwidgetDevice:
489
655
 
490
656
  @property # type: ignore
491
657
  def friendly_name(self) -> str:
492
- """Return a friendly description of the device"""
658
+ """Return a friendly description of the device."""
493
659
  return self._friendly_name
494
660
 
495
661
  @property # type: ignore
496
662
  def is_on(self) -> bool:
497
663
  """Return whether device is on."""
498
664
  _LOGGER.debug("SwidgetDevice.is_on called")
499
- dimmer_state = self.assemblies['host'].components["0"].functions['toggle']["state"]
665
+ dimmer_state = (
666
+ self.assemblies["host"].components["0"].functions["toggle"]["state"]
667
+ )
500
668
  if dimmer_state == "on":
501
669
  return True
502
670
  return False
@@ -504,7 +672,6 @@ class SwidgetDevice:
504
672
  async def turn_on(self) -> None:
505
673
  """Turn the device on."""
506
674
  _LOGGER.debug("SwidgetDevice.turn_on() called")
507
- self.assemblies['host'].components["0"].functions['toggle']["state"] = "on"
508
675
  await self.send_command(
509
676
  assembly="host", component="0", function="toggle", command={"state": "on"}
510
677
  )
@@ -512,7 +679,6 @@ class SwidgetDevice:
512
679
  async def turn_off(self) -> None:
513
680
  """Turn the device off."""
514
681
  _LOGGER.debug("SwidgetDevice.turn_off() called")
515
- self.assemblies['host'].components["0"].functions['toggle']["state"] = "off"
516
682
  await self.send_command(
517
683
  assembly="host", component="0", function="toggle", command={"state": "off"}
518
684
  )
@@ -520,24 +686,30 @@ class SwidgetDevice:
520
686
  async def turn_on_usb_insert(self) -> None:
521
687
  """Turn the USB insert on."""
522
688
  _LOGGER.debug("SwidgetDevice.turn_on_usb_insert() called")
523
- self.assemblies['insert'].components["usb"].functions['toggle']["state"] = "on"
524
689
  await self.send_command(
525
- assembly="insert", component="usb", function="toggle", command={"state": "on"}
690
+ assembly="insert",
691
+ component="usb",
692
+ function="toggle",
693
+ command={"state": "on"},
526
694
  )
527
695
 
528
696
  async def turn_off_usb_insert(self) -> None:
529
697
  """Turn the USB insert off."""
530
698
  _LOGGER.debug("SwidgetDevice.turn_off_usb_insert() called")
531
- self.assemblies['insert'].components["usb"].functions['toggle']["state"] = "off"
532
699
  await self.send_command(
533
- assembly="insert", component="usb", function="toggle", command={"state": "off"}
700
+ assembly="insert",
701
+ component="usb",
702
+ function="toggle",
703
+ command={"state": "off"},
534
704
  )
535
705
 
536
706
  @property # type: ignore
537
707
  def usb_is_on(self) -> bool:
538
708
  """Return whether USB is on."""
539
709
  _LOGGER.debug("SwidgetDevice.usb_is_on called")
540
- usb_state = self.assemblies['insert'].components["usb"].functions['toggle']["state"]
710
+ usb_state = (
711
+ self.assemblies["insert"].components["usb"].functions["toggle"]["state"]
712
+ )
541
713
  if usb_state == "on":
542
714
  return True
543
715
  return False
@@ -560,6 +732,8 @@ class SwidgetDevice:
560
732
 
561
733
 
562
734
  class SwidgetAssembly:
735
+ """Class to represent parts of the Swidget device."""
736
+
563
737
  def __init__(self, summary: dict):
564
738
  self.type = summary["type"]
565
739
  self.components = {
@@ -570,5 +744,38 @@ class SwidgetAssembly:
570
744
 
571
745
 
572
746
  class SwidgetComponent:
747
+ """Component-level representation of a Swidget Assembly."""
748
+
573
749
  def __init__(self, functions):
574
750
  self.functions = {f: None for f in functions}
751
+
752
+
753
+ class DeviceConfiguration:
754
+ """A class to represent and manage the Swidget device configuration."""
755
+
756
+ def __init__(self, config_dict={}):
757
+ self._config_dict = config_dict
758
+
759
+ def config_populated(self) -> bool:
760
+ """Return if configuration has been retrieved from the device."""
761
+ return self._config_dict == {}
762
+
763
+ @property
764
+ def config(self):
765
+ """Return the config dictionary for the device."""
766
+ return self._config_dict
767
+
768
+ @config.setter
769
+ def config(self, config_dict={}):
770
+ self._config_dict = config_dict
771
+
772
+ def update_config(self, updates):
773
+ """Update configuration with the given updates."""
774
+ for key, value in updates.items():
775
+ self._set_nested_value(self._config_dict, key.split("."), value)
776
+
777
+ def _set_nested_value(self, config, keys, value):
778
+ """Set nested value in the configuration."""
779
+ for key in keys[:-1]:
780
+ config = config.setdefault(key, {})
781
+ config[keys[-1]] = value