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.
- {python_swidget-1.2.2.dist-info → python_swidget-1.2.4.dist-info}/METADATA +3 -2
- python_swidget-1.2.4.dist-info/RECORD +16 -0
- swidget/__init__.py +10 -5
- swidget/cli.py +55 -43
- swidget/discovery.py +35 -8
- swidget/provision.py +57 -37
- swidget/swidgetdevice.py +302 -95
- swidget/swidgetdimmer.py +37 -14
- swidget/swidgetoutlet.py +19 -7
- swidget/swidgetswitch.py +19 -7
- swidget/swidgettimerswitch.py +23 -7
- swidget/websocket.py +39 -18
- python_swidget-1.2.2.dist-info/RECORD +0 -16
- {python_swidget-1.2.2.dist-info → python_swidget-1.2.4.dist-info}/WHEEL +0 -0
- {python_swidget-1.2.2.dist-info → python_swidget-1.2.4.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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) ->
|
|
124
|
+
async def stop(self) -> bool:
|
|
107
125
|
"""Stop the websocket."""
|
|
108
126
|
_LOGGER.debug("SwidgetDevice.stop()")
|
|
109
|
-
if hasattr(self,
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
156
|
-
|
|
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
|
-
"""
|
|
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[
|
|
177
|
-
self.insert_type = self.assemblies[
|
|
178
|
-
self.id = self.assemblies[
|
|
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[
|
|
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
|
-
"""
|
|
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(
|
|
202
|
-
|
|
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
|
-
"""
|
|
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(
|
|
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(
|
|
258
|
-
|
|
259
|
-
|
|
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[
|
|
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[
|
|
479
|
+
for id, properties in self.assemblies["host"].components.items():
|
|
400
480
|
try:
|
|
401
|
-
return_dict[f"power_{id}"] = properties.functions[
|
|
402
|
-
|
|
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
|
|
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[
|
|
411
|
-
total_consumption += properties.functions[
|
|
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({
|
|
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[
|
|
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[
|
|
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
|
|
535
|
+
for function, data in (
|
|
536
|
+
self.assemblies["insert"].components[function].functions.items()
|
|
537
|
+
):
|
|
450
538
|
if function == "occupied":
|
|
451
|
-
return_values[function] = data[
|
|
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[
|
|
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
|
|
560
|
+
return (
|
|
561
|
+
self.assemblies["insert"]
|
|
562
|
+
.components[function]
|
|
563
|
+
.functions["occupied"]["state"]
|
|
564
|
+
)
|
|
462
565
|
else:
|
|
463
|
-
return
|
|
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
|
|
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 =
|
|
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",
|
|
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",
|
|
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 =
|
|
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
|