python-swidget 0.0.32__tar.gz → 1.0.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: 0.0.32
3
+ Version: 1.0.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
@@ -31,3 +31,21 @@ Description-Content-Type: text/markdown
31
31
  # python-swidget
32
32
  A library to manage Swidget smart devices
33
33
 
34
+ # Basic Usage
35
+
36
+ ## Connect to the device using http/ https
37
+ ```
38
+ dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=False)
39
+ dev.update()
40
+ dev.turn_on()
41
+ dev.close()
42
+ ```
43
+
44
+ ## Connect to the device using websockets
45
+ ```
46
+ dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=True)
47
+ dev.start()
48
+ dev.update()
49
+ dev.turn_on()
50
+ dev.close()
51
+ ```
@@ -0,0 +1,21 @@
1
+ # python-swidget
2
+ A library to manage Swidget smart devices
3
+
4
+ # Basic Usage
5
+
6
+ ## Connect to the device using http/ https
7
+ ```
8
+ dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=False)
9
+ dev.update()
10
+ dev.turn_on()
11
+ dev.close()
12
+ ```
13
+
14
+ ## Connect to the device using websockets
15
+ ```
16
+ dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=True)
17
+ dev.start()
18
+ dev.update()
19
+ dev.turn_on()
20
+ dev.close()
21
+ ```
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-swidget"
3
- version = "0.0.32"
3
+ version = "1.0.1"
4
4
  description = "Python API for Swidget smart devices"
5
5
  license = "GPL-3.0-or-later"
6
6
  authors = ["Swidget"]
@@ -1,5 +1,4 @@
1
1
  """python-swidget cli tool."""
2
- import asyncio
3
2
  import logging
4
3
  import sys
5
4
  from pprint import pformat as pf
@@ -47,6 +46,10 @@ pass_dev = click.make_pass_decorator(SwidgetDevice)
47
46
  envvar="SWIDGET_DEBUG",
48
47
  default=False,
49
48
  is_flag=True)
49
+ @click.option("--http_only",
50
+ envvar="SWIDGET_HTTP_ONLY",
51
+ default=True,
52
+ is_flag=True)
50
53
  @click.option(
51
54
  "--type",
52
55
  envvar="SWIDGET_TYPE",
@@ -55,7 +58,7 @@ pass_dev = click.make_pass_decorator(SwidgetDevice)
55
58
  )
56
59
  @click.version_option(package_name="python-swidget")
57
60
  @click.pass_context
58
- async def cli(ctx, host, password, debug, type):
61
+ async def cli(ctx, host, password, debug, http_only, type):
59
62
  """A tool for controlling Swidget smart home devices.""" # noqa
60
63
  # no need to perform any checks if we are just displaying the help
61
64
  if sys.argv[-1] == "--help":
@@ -68,7 +71,6 @@ async def cli(ctx, host, password, debug, type):
68
71
 
69
72
  if ctx.invoked_subcommand == "discover" or ctx.invoked_subcommand == "wifi":
70
73
  return
71
-
72
74
  if host is None:
73
75
  click.echo("No host name given, trying discovery..")
74
76
  await ctx.invoke(discover)
@@ -77,15 +79,16 @@ async def cli(ctx, host, password, debug, type):
77
79
  dev = TYPE_TO_CLASS[type](host=host,
78
80
  token_name='x-secret-key',
79
81
  secret_key=password,
80
- ssl=False,
82
+ use_https=http_only,
81
83
  use_websockets=False)
82
84
  else:
83
85
  click.echo("No --type defined, discovering...")
84
86
  dev = await discover_single(host=host,
85
87
  token_name='x-secret-key',
86
88
  password=password,
87
- ssl=False,
89
+ use_https=http_only,
88
90
  use_websockets=False)
91
+ await dev.update()
89
92
 
90
93
  @asynccontextmanager
91
94
  async def async_wrapped_device(dev: SwidgetDevice):
@@ -104,7 +107,7 @@ async def cli(ctx, host, password, debug, type):
104
107
  def wifi():
105
108
  """Commands to control wifi settings."""
106
109
 
107
- #@ click.argument("ssid")
110
+
108
111
  @wifi.command()
109
112
  @click.option("--ssid", prompt=True, hide_input=False)
110
113
  @click.option("--network_password", prompt=True, hide_input=True)
@@ -124,7 +127,7 @@ def join(ssid, network_password, secret_key, friendly_name):
124
127
 
125
128
 
126
129
  @cli.command()
127
- @click.option("--timeout", default=5, required=False)
130
+ @click.option("--timeout", default=10, required=False)
128
131
  @click.pass_context
129
132
  async def discover(ctx, timeout):
130
133
  """Discover devices in the network."""
@@ -225,6 +228,20 @@ async def blink(dev):
225
228
  return await dev.blink()
226
229
 
227
230
 
231
+ @cli.command()
232
+ @pass_dev
233
+ async def ping(dev):
234
+ """Ping the device"""
235
+ click.echo(f"Pinging the device")
236
+ try:
237
+ result = await dev.ping()
238
+ if result == 200:
239
+ click.echo("Successfully pinged device")
240
+ else:
241
+ click.echo(result.status_code)
242
+ except:
243
+ click.echo("Unable to ping device")
244
+
228
245
  @cli.command()
229
246
  @pass_dev
230
247
  async def on(dev: SwidgetDevice):
@@ -240,6 +257,7 @@ async def off(dev: SwidgetDevice):
240
257
  click.echo(f"Turning off {dev.friendly_name}")
241
258
  return await dev.turn_off()
242
259
 
260
+
243
261
  @cli.command()
244
262
  @pass_dev
245
263
  async def enable_debug_server(dev: SwidgetDevice):
@@ -248,5 +266,34 @@ async def enable_debug_server(dev: SwidgetDevice):
248
266
  return await dev.enable_debug_server()
249
267
 
250
268
 
269
+ @cli.command()
270
+ @pass_dev
271
+ async def check_for_updates(dev: SwidgetDevice):
272
+ click.echo("Contacting Swidget servers to fetch for updates...")
273
+ available_updates = await dev.check_for_updates()
274
+ if len(available_updates['updates']) == 0:
275
+ click.echo("No available updates")
276
+ else:
277
+ click.echo("The following versions are available to update to")
278
+ for version in available_updates['updates']:
279
+ click.echo(click.style(f"\t+ {version}", fg="green"))
280
+
281
+
282
+ @cli.command()
283
+ @click.option("--version", required=False)
284
+ @pass_dev
285
+ async def upgrade(dev: SwidgetDevice, version: str):
286
+ if version is None:
287
+ click.echo("Contacting Swidget servers to fetch for latest version")
288
+ available_updates = await dev.check_for_updates()
289
+ if len(available_updates['updates']) == 0:
290
+ click.echo("No available updates")
291
+ else:
292
+ version = available_updates[-1]
293
+ click.echo(f"Upgrading to version: {version}")
294
+ response = await dev.update_version(version)
295
+ click.echo(response)
296
+
297
+
251
298
  if __name__ == "__main__":
252
299
  cli()
@@ -65,21 +65,26 @@ async def discover_devices(timeout=RESPONSE_SEC):
65
65
  return devices
66
66
 
67
67
 
68
- async def discover_single(host: str, token_name: str, password: str, ssl: bool, use_websockets: bool) -> SwidgetDevice:
68
+ async def discover_single(host: str, token_name: str, password: str, use_https: bool, use_websockets: bool) -> SwidgetDevice:
69
69
  """Discover a single device by the given IP address.
70
70
 
71
71
  :param host: Hostname of device to query
72
72
  :rtype: SwidgetDevice
73
73
  :return: Object for querying/controlling found device.
74
74
  """
75
- swidget_device = SwidgetDevice(host, token_name, password, ssl, use_websockets)
75
+ _LOGGER.debug(f"Checking for device at {host}")
76
+ swidget_device = SwidgetDevice(host, token_name, password, use_https, use_websockets=False)
77
+ _LOGGER.debug(f"Asking {host} for summary data")
76
78
  await swidget_device.get_summary()
77
79
  device_type = swidget_device.device_type
80
+ _LOGGER.debug(f"{host} is of type {device_type}")
78
81
  await swidget_device.stop()
79
82
 
83
+ _LOGGER.debug(f"Creating new device class of type: {device_type}")
80
84
  device_class = _get_device_class(device_type)
81
- dev = device_class(host, token_name, password, ssl, use_websockets)
82
- await dev.update()
85
+ _LOGGER.debug(f"{device_class}")
86
+ dev = device_class(host, token_name, password, use_https, use_websockets)
87
+ await dev.start()
83
88
  return dev
84
89
 
85
90
 
@@ -43,6 +43,7 @@ def send_credentials(device_name, ssid, network_password, secret_key):
43
43
  time.sleep(5)
44
44
  sentProvisionRequestAttempts += 1
45
45
 
46
+
46
47
  def verify_connect_result(key):
47
48
  headers = {'x-secret-key': key}
48
49
  connect_success = False
@@ -3,6 +3,7 @@ import logging
3
3
  import time
4
4
 
5
5
  from aiohttp import ClientSession, TCPConnector
6
+ import asyncio
6
7
  from enum import Enum
7
8
  from typing import Dict, List, Set
8
9
 
@@ -11,7 +12,6 @@ from .websocket import SwidgetWebsocket
11
12
 
12
13
  _LOGGER = logging.getLogger(__name__)
13
14
 
14
-
15
15
  class DeviceType(Enum):
16
16
  """Device type enum."""
17
17
 
@@ -24,16 +24,20 @@ class DeviceType(Enum):
24
24
 
25
25
 
26
26
  class SwidgetDevice:
27
- def __init__(self, host, token_name, secret_key, ssl=False, use_websockets=True):
27
+ def __init__(self, host, token_name, secret_key, use_https=True, use_websockets=True):
28
28
  self.token_name = token_name
29
29
  self.ip_address = host
30
- self.ssl = ssl
30
+
31
+ self.use_https = use_https
32
+ self.uri_scheme = 'https' if self.use_https is True else 'http'
31
33
  self.secret_key = secret_key
32
34
  self.use_websockets = use_websockets
33
35
  self.device_type = DeviceType.Unknown
34
36
  self._friendly_name = "Unknown Swidget Device"
35
- headers = {self.token_name: self.secret_key}
36
- connector = TCPConnector(verify_ssl=ssl, force_close=True)
37
+ self.assemblies = {}
38
+ headers = {self.token_name: self.secret_key,
39
+ 'Connection': 'keep-alive'}
40
+ connector = TCPConnector(verify_ssl=False, force_close=True)
37
41
  self._session = ClientSession(headers=headers, connector=connector)
38
42
  self._last_update = None
39
43
  if self.use_websockets:
@@ -45,37 +49,65 @@ class SwidgetDevice:
45
49
  session=self._session)
46
50
 
47
51
  def get_websocket(self):
48
- return self._websocket
52
+ if self.use_websockets:
53
+ return self._websocket
54
+ return None
49
55
 
50
56
  def set_countdown_timer(self, minutes):
51
57
  raise NotImplementedError()
52
58
 
59
+ async def connect(self):
60
+ await self._websocket.connect()
61
+
62
+ async def start(self):
63
+ """Start the websocket."""
64
+ _LOGGER.debug("SwidgetDevice.start()")
65
+ if self.use_websockets:
66
+ _LOGGER.debug("Calling self._websocket.connect()")
67
+ await self._websocket.connect()
68
+ _LOGGER.debug("Calling self.update() ")
69
+ await self.update()
70
+
53
71
  async def stop(self):
54
72
  """Stop the websocket."""
73
+ _LOGGER.debug("SwidgetDevice.stop()")
55
74
  if hasattr(self, '_websocket'):
56
75
  await self._websocket.close()
57
76
  await self._session.close()
58
77
 
78
+ async def close(self):
79
+ await self.stop()
80
+
59
81
  async def message_callback(self, message):
60
82
  """Entrypoint for a websocket callback"""
83
+ _LOGGER.debug("SwidgetDevice.message_callback() called")
61
84
  if message["request_id"] == "summary":
85
+ _LOGGER.debug("Calling SwidgetDevice.process_summary()")
62
86
  await self.process_summary(message)
63
87
  elif message["request_id"] == "state" or message["request_id"] == "DYNAMIC_UPDATE" or message["request_id"] == "command":
88
+ _LOGGER.debug("Calling SwidgetDevice.process_state()")
64
89
  await self.process_state(message)
90
+ else:
91
+ _LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
65
92
 
66
93
  async def get_summary(self):
67
94
  """Get a summary of the device over HTTP"""
68
- try:
95
+ _LOGGER.debug("SwidgetDevice.get_summary() called")
96
+ if self.use_websockets:
97
+ _LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
98
+ await self._websocket.send_str(json.dumps({"type": "summary", "request_id": "summary"}))
99
+ else:
100
+ _LOGGER.debug("In http mode. Sending get_summary() command over http")
69
101
  async with self._session.get(
70
- url=f"https://{self.ip_address}/api/v1/summary", ssl=self.ssl
102
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
71
103
  ) as response:
72
104
  summary = await response.json()
73
105
  await self.process_summary(summary)
74
- except:
75
- raise SwidgetException("Unable to connect to device")
76
106
 
77
107
  async def process_summary(self, summary):
78
108
  """ Process the data around the summary of the device"""
109
+ _LOGGER.debug("SwidgetDevice.process_summary() called")
110
+ _LOGGER.debug(f"Summary to process: {summary}")
79
111
  self.model = summary["model"]
80
112
  self.mac_address = summary["mac"]
81
113
  self.version = summary["version"]
@@ -89,9 +121,10 @@ class SwidgetDevice:
89
121
  self._last_update = int(time.time())
90
122
 
91
123
  async def get_friendly_name(self):
124
+ _LOGGER.debug("SwidgetDevice.get_friendly_name() called")
92
125
  try:
93
126
  async with self._session.get(
94
- url=f"https://{self.ip_address}/api/v1/name", ssl=self.ssl
127
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
95
128
  ) as response:
96
129
  name = await response.json()
97
130
  except Exception:
@@ -99,24 +132,33 @@ class SwidgetDevice:
99
132
  await self.process_friendly_name(name['name'])
100
133
 
101
134
  async def process_friendly_name(self, name):
135
+ _LOGGER.debug("SwidgetDevice.process_friendly_name() called")
102
136
  self._friendly_name = name
137
+ self._last_update = int(time.time())
103
138
 
104
139
  async def get_state(self):
105
140
  """ Get the state of the device over HTTP"""
106
- async with self._session.get(
107
- url=f"https://{self.ip_address}/api/v1/state", ssl=self.ssl
108
- ) as response:
109
- state = await response.json()
110
- await self.process_state(state)
141
+ _LOGGER.debug("SwidgetDevice.get_state() called")
142
+ if self.use_websockets:
143
+ _LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
144
+ await self._websocket.send_str(json.dumps({"type": "state", "request_id": "state"}))
145
+ else:
146
+ _LOGGER.debug("In http mode. Sending get_summary() command over http")
147
+ async with self._session.get(
148
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/state", ssl=False
149
+ ) as response:
150
+ state = await response.json()
151
+ await self.process_state(state)
111
152
 
112
153
  async def process_state(self, state):
113
154
  """ Process any information about the state of the device or insert"""
114
155
  # State is not always in the state (during callback)
156
+ _LOGGER.debug("SwidgetDevice.process_state() called")
157
+ _LOGGER.debug(f"State to process: {state}")
115
158
  try:
116
159
  self.rssi = state["connection"]["rssi"]
117
160
  except:
118
161
  pass
119
-
120
162
  for assembly in self.assemblies:
121
163
  for id, component in self.assemblies[assembly].components.items():
122
164
  try:
@@ -126,37 +168,49 @@ class SwidgetDevice:
126
168
  self._last_update = int(time.time())
127
169
 
128
170
  async def update(self):
171
+ _LOGGER.debug("SwidgetDevice.update() called")
129
172
  if self._last_update is None:
130
173
  _LOGGER.debug("Performing the initial update to obtain sysinfo")
131
- await self.get_summary()
132
- await self.get_state()
133
- if self._friendly_name == "Unknown Swidget Device":
134
- await self.get_friendly_name()
174
+ await self.get_summary()
175
+ await self.get_state()
176
+ if self._friendly_name == "Unknown Swidget Device":
177
+ await self.get_friendly_name()
178
+ elif (int(time.time()) - self._last_update) < 5:
179
+ _LOGGER.debug("update() recently called, not executing")
180
+ else:
181
+ _LOGGER.debug("Requesting an update of the device")
182
+ await self.get_summary()
183
+ await self.get_state()
135
184
 
136
185
  async def send_config(self, payload: dict):
137
- data = json.dumps({"type":"config","request_id":"abcd", "payload": payload})
186
+ _LOGGER.debug("SwidgetDevice.send_config() called")
187
+ data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
138
188
  await self._websocket.send_str(data)
139
189
 
140
190
  async def send_command(
141
191
  self, assembly: str, component: str, function: str, command: dict
142
192
  ):
193
+ _LOGGER.debug("SwidgetDevice.send_command() called")
143
194
  """Send a command to the Swidget device either using a HTTP call or the existing websocket"""
144
195
  data = {assembly: {"components": {component: {function: command}}}}
145
-
196
+ _LOGGER.debug(f"Command to send: {data}")
146
197
  if self.use_websockets:
198
+ _LOGGER.debug("In websocket mode. Sending command over websocket")
147
199
  data = json.dumps({"type": "command",
148
200
  "request_id": "command",
149
201
  "payload": data
150
202
  })
151
203
  await self._websocket.send_str(data)
152
204
  else:
205
+ _LOGGER.debug("NOT in websocket mode, sending command over HTTP")
153
206
  async with self._session.post(
154
- url=f"https://{self.ip_address}/api/v1/command",
155
- ssl=self.ssl,
207
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/command",
208
+ ssl=False,
156
209
  data=json.dumps(data),
157
210
  ) as response:
158
211
  state = await response.json()
159
212
 
213
+ # Do a hard set of the new state of the device. May change this in the future
160
214
  function_value = state[assembly]["components"][component][function]
161
215
  self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
162
216
 
@@ -165,12 +219,13 @@ class SwidgetDevice:
165
219
 
166
220
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
167
221
  """
222
+ _LOGGER.debug("SwidgetDevice.ping() called")
168
223
  try:
169
224
  async with self._session.get(
170
- url=f"https://{self.ip_address}/ping",
171
- ssl=self.ssl
225
+ url=f"{self.uri_scheme}://{self.ip_address}/ping",
226
+ ssl=False
172
227
  ) as response:
173
- return response.text
228
+ return response.status
174
229
  except:
175
230
  raise SwidgetException
176
231
 
@@ -179,12 +234,13 @@ class SwidgetDevice:
179
234
 
180
235
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
181
236
  """
237
+ _LOGGER.debug("SwidgetDevice.blink() called")
182
238
  try:
183
239
  async with self._session.get(
184
- url=f"https://{self.ip_address}/blink",
185
- ssl=self.ssl
240
+ url=f"{self.uri_scheme}://{self.ip_address}/blink",
241
+ ssl=False
186
242
  ) as response:
187
- return response.text
243
+ return await response.json()
188
244
  except:
189
245
  raise SwidgetException
190
246
 
@@ -193,12 +249,13 @@ class SwidgetDevice:
193
249
 
194
250
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
195
251
  """
252
+ _LOGGER.debug("SwidgetDevice.enable_debug_server() called")
196
253
  try:
197
254
  async with self._session.get(
198
- url=f"https://{self.ip_address}/debug?x-secret-key={self.secret_key}",
199
- ssl=self.ssl
255
+ url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
256
+ ssl=False
200
257
  ) as response:
201
- return response.text
258
+ return await response.json()
202
259
  except:
203
260
  raise SwidgetException
204
261
 
@@ -210,10 +267,43 @@ class SwidgetDevice:
210
267
  try:
211
268
 
212
269
  async with self._session.delete(
213
- url=f"https://{self.ip_address}/api/v1/reset",
214
- ssl=self.ssl
270
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset",
271
+ ssl=False
215
272
  ) as response:
216
- return response.text
273
+ return await response.json()
274
+ except:
275
+ raise SwidgetException
276
+
277
+ async def check_for_updates(self):
278
+ """Tell the device to contact the Swidget servers to see if there is an available update
279
+
280
+ :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
281
+ """
282
+ try:
283
+
284
+ async with self._session.get(
285
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
286
+ ssl=False
287
+ ) as response:
288
+ return await response.json()
289
+ except:
290
+ raise SwidgetException
291
+
292
+ async def update_version(self, version):
293
+ """Tell the device to download and apply an update
294
+
295
+ :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
296
+ """
297
+ try:
298
+ data = {
299
+ "version": version
300
+ }
301
+ async with self._session.post(
302
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
303
+ ssl=False,
304
+ data=json.dumps(data)
305
+ ) as response:
306
+ return await response
217
307
  except:
218
308
  raise SwidgetException
219
309
 
@@ -329,7 +419,7 @@ class SwidgetDevice:
329
419
  @property
330
420
  def is_dimmable(self) -> bool:
331
421
  """Return True if the device is dimmable."""
332
- return False
422
+ return self.is_dimmer
333
423
 
334
424
  @property # type: ignore
335
425
  def friendly_name(self) -> str:
@@ -346,11 +436,14 @@ class SwidgetDevice:
346
436
 
347
437
  async def turn_on(self):
348
438
  """Turn the device on."""
439
+ _LOGGER.debug("SwidgetDevice.turn_on() called")
349
440
  await self.send_command(
350
441
  assembly="host", component="0", function="toggle", command={"state": "on"}
351
442
  )
443
+
352
444
  async def turn_off(self):
353
445
  """Turn the device off."""
446
+ _LOGGER.debug("SwidgetDevice.turn_off() called")
354
447
  await self.send_command(
355
448
  assembly="host", component="0", function="toggle", command={"state": "off"}
356
449
  )
@@ -6,14 +6,13 @@ from swidget.swidgetdevice import (
6
6
  )
7
7
  from swidget.exceptions import SwidgetException
8
8
 
9
+ _LOGGER = logging.getLogger(__name__)
9
10
 
10
11
 
11
- log = logging.getLogger(__name__)
12
-
13
12
  class SwidgetDimmer(SwidgetDevice):
14
13
 
15
- def __init__(self, host, token_name: str, secret_key: str, ssl: bool, use_websockets: bool) -> None:
16
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
14
+ def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
15
+ super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
17
16
  self._device_type = "dimmer"
18
17
 
19
18
  @property # type: ignore
@@ -31,11 +30,13 @@ class SwidgetDimmer(SwidgetDevice):
31
30
 
32
31
  async def set_brightness(self, brightness):
33
32
  """Set the brightness of the device."""
33
+ _LOGGER.debug("SwidgetDimmer.set_brightness() called")
34
34
  await self.send_command(
35
35
  assembly="host", component="0", function="level", command={"now": brightness}
36
36
  )
37
37
 
38
38
  async def set_default_brightness(self, brightness):
39
+ _LOGGER.debug("SwidgetDimmer.set_default_brightness() called")
39
40
  await self.send_command(
40
41
  assembly="host", component="0", function="level", command={"default": brightness}
41
42
  )
@@ -6,6 +6,6 @@ from swidget.swidgetdevice import (
6
6
 
7
7
  class SwidgetOutlet(SwidgetDevice):
8
8
 
9
- def __init__(self, host, token_name: str, secret_key: str, ssl: bool, use_websockets: bool) -> None:
10
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
9
+ def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
10
+ super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
11
11
  self._device_type = DeviceType.Outlet
@@ -6,8 +6,8 @@ from swidget.swidgetdevice import (
6
6
 
7
7
  class SwidgetSwitch(SwidgetDevice):
8
8
 
9
- def __init__(self, host, token_name: str, secret_key: str, ssl: bool, use_websockets: bool) -> None:
10
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
9
+ def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
10
+ super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
11
11
  self._device_type = DeviceType.Switch
12
12
 
13
13
  async def current_consumption(self) -> float:
@@ -1,17 +1,21 @@
1
+ import logging
1
2
  from swidget.swidgetdevice import (
2
3
  DeviceType,
3
4
  )
4
5
  from swidget.swidgetswitch import SwidgetSwitch
5
6
 
7
+ _LOGGER = logging.getLogger(__name__)
8
+
6
9
 
7
10
  class SwidgetTimerSwitch(SwidgetSwitch):
8
11
 
9
- def __init__(self, host, token_name: str, secret_key: str, ssl: bool, use_websockets: bool) -> None:
10
- super().__init__(host=host, token_name=token_name, secret_key=secret_key, ssl=ssl, use_websockets=use_websockets)
12
+ def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
13
+ super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
11
14
  self._device_type = DeviceType.TimerSwitch
12
15
 
13
16
  async def set_countdown_timer(self, minutes):
14
17
  """Set the countdown timer."""
18
+ _LOGGER.debug("SwidgetTimerSwitch.set_brightness() called")
15
19
  await self.send_command(
16
20
  assembly="host", component="0", function="timer", command={"duration": minutes}
17
21
  )
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+ import aiohttp
3
+ from aiohttp import ClientWebSocketResponse, WSMsgType
4
+ import logging
5
+ import socket
6
+
7
+ _LOGGER = logging.getLogger(__name__)
8
+
9
+
10
+ async def cancel_task(*tasks: asyncio.Task | None) -> None:
11
+ """Cancel task(s)."""
12
+ for task in tasks:
13
+ if task is not None and not task.done():
14
+ task.cancel()
15
+ try:
16
+ await task
17
+ except asyncio.CancelledError:
18
+ pass
19
+
20
+
21
+ class SwidgetWebsocket:
22
+ """A websocket connection to a Swidget Device"""
23
+
24
+ # pylint: disable=too-many-instance-attributes
25
+ _client: aiohttp.ClientWebSocketResponse | None = None
26
+
27
+ def __init__(
28
+ self,
29
+ host,
30
+ token_name,
31
+ secret_key,
32
+ callback,
33
+ session=None,
34
+ use_security=True,
35
+ ):
36
+
37
+ self.session = session or aiohttp.ClientSession()
38
+ self.use_security = use_security
39
+ self.uri = self.get_uri(host, token_name, secret_key)
40
+ self.callback = callback
41
+ self._verify_ssl = False
42
+ self._state = None
43
+ self.failed_attempts = 0
44
+ self._error_reason = None
45
+ self.headers = {'Connection': 'Upgrade'}
46
+ self._receiver_task: asyncio.Task | None = None
47
+
48
+ @property
49
+ def connected(self) -> bool:
50
+ return self._client is not None and not self._client.closed
51
+
52
+ @property
53
+ def websocket(self) -> ClientWebSocketResponse | None:
54
+ """Return the web socket."""
55
+ return self._ws
56
+
57
+ def get_uri(self, host, token_name, secret_key):
58
+ """Generate the websocket URI"""
59
+ if self.use_security:
60
+ return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
61
+ else:
62
+ return f"ws://{host}/api/v1/sock?{token_name}={secret_key}"
63
+
64
+ async def connect(self) -> None:
65
+ _LOGGER.debug("websocket.connect() called")
66
+ """Create a new connection and, optionally, start the monitor."""
67
+ await cancel_task(self._receiver_task)
68
+ if self.connected:
69
+ _LOGGER.debug("Websocket already connected")
70
+ return
71
+
72
+ if not self.session:
73
+ raise
74
+
75
+ try:
76
+ self._client = await self.session.ws_connect(url=self.uri, headers=self.headers, verify_ssl=self._verify_ssl, heartbeat=30)
77
+ _LOGGER.debug("Websocket now connected")
78
+ except (
79
+ aiohttp.WSServerHandshakeError,
80
+ aiohttp.ClientConnectionError,
81
+ socket.gaierror,
82
+ ) as exception:
83
+ msg = (
84
+ "Error occurred while communicating with WLED device"
85
+ f" on WebSocket at {self.host}"
86
+ )
87
+ raise(msg)
88
+ self._receiver_task = asyncio.ensure_future(self.listen())
89
+
90
+ async def close(self) -> None:
91
+ _LOGGER.debug("websocket.close() called")
92
+ if not self._client or not self.connected:
93
+ return
94
+ await self._client.close()
95
+
96
+ async def send_str(self, message):
97
+ """Send a message through the websocket."""
98
+ _LOGGER.debug("websocket.send_str() called")
99
+ message = str(message)
100
+ _LOGGER.debug(f"Sending messsage over websocket: {message}")
101
+ await self._client.send_str(f'{message}')
102
+
103
+ async def listen(self):
104
+ _LOGGER.debug("websocket.listen() called")
105
+ if not self._client or not self.connected:
106
+ raise
107
+
108
+ while not self._client.closed:
109
+ message = await self._client.receive()
110
+
111
+ if message.type == aiohttp.WSMsgType.ERROR:
112
+ raise
113
+
114
+ if message.type == aiohttp.WSMsgType.TEXT:
115
+ message_data = message.json()
116
+ await self.callback(message_data)
117
+
118
+ if message.type in (
119
+ aiohttp.WSMsgType.CLOSE,
120
+ aiohttp.WSMsgType.CLOSED,
121
+ aiohttp.WSMsgType.CLOSING,
122
+ ):
123
+ _LOGGER.debug("Connection to the Swidget WebSocket on has been closed")
@@ -1,2 +0,0 @@
1
- # python-swidget
2
- A library to manage Swidget smart devices
@@ -1,124 +0,0 @@
1
- import aiohttp
2
- import asyncio
3
- import logging
4
- import json
5
-
6
-
7
-
8
- _LOGGER = logging.getLogger(__name__)
9
-
10
- ERROR_AUTH_FAILURE = "Authorization failure"
11
- ERROR_TOO_MANY_RETRIES = "Too many retries"
12
- ERROR_UNKNOWN = "Unknown"
13
-
14
- MAX_FAILED_ATTEMPTS = 5
15
-
16
- STATE_CONNECTED = "connected"
17
- STATE_DISCONNECTED = "disconnected"
18
- STATE_STARTING = "starting"
19
- STATE_STOPPED = "stopped"
20
-
21
-
22
- class SwidgetWebsocket:
23
- """A websocket connection to a Swidget Device"""
24
-
25
- # pylint: disable=too-many-instance-attributes
26
-
27
- def __init__(
28
- self,
29
- host,
30
- token_name,
31
- secret_key,
32
- callback,
33
- session=None,
34
- verify_ssl=False,
35
- ):
36
-
37
- self.session = session or aiohttp.ClientSession()
38
- self.uri = self._get_uri(host, token_name, secret_key)
39
- self.callback = callback
40
- self._ssl = False if verify_ssl is False else None
41
- self._state = None
42
- self.failed_attempts = 0
43
- self._error_reason = None
44
-
45
- @property
46
- def state(self):
47
- """Return the current state."""
48
- return self._state
49
-
50
- @state.setter
51
- def state(self, value):
52
- """Set the state."""
53
- self._state = value
54
-
55
- @staticmethod
56
- def _get_uri(host, token_name, secret_key):
57
- """Generate the websocket URI"""
58
- return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
59
-
60
- async def running(self):
61
- """Open a persistent websocket connection and act on events."""
62
- self.state = STATE_STARTING
63
-
64
- try:
65
- headers = {'Connection': 'Upgrade'}
66
- async with self.session.ws_connect(self.uri, headers=headers, verify_ssl=False, heartbeat=30) as self.ws_client:
67
- self.state = STATE_CONNECTED
68
- self.failed_attempts = 0
69
- await self.send_str(json.dumps({"type": "summary", "request_id": "1"}))
70
- await self.send_str(json.dumps({"type": "state", "request_id": "2"}))
71
- async for message in self.ws_client:
72
- if self.state == STATE_STOPPED:
73
- break
74
- if message.type == aiohttp.WSMsgType.TEXT:
75
- msg = message.json()
76
- await self.callback(msg)
77
-
78
- elif message.type == aiohttp.WSMsgType.CLOSED:
79
- break
80
-
81
- elif message.type == aiohttp.WSMsgType.ERROR:
82
- break
83
- except aiohttp.ClientResponseError as error:
84
- if error.code == 401:
85
- _LOGGER.error(f"Credentials rejected: {error}")
86
- self._error_reason = ERROR_AUTH_FAILURE
87
- else:
88
- _LOGGER.error(f"Unexpected response received: {error}")
89
- self._error_reason = ERROR_UNKNOWN
90
- self.state = STATE_STOPPED
91
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as error:
92
- if self.failed_attempts >= MAX_FAILED_ATTEMPTS:
93
- self._error_reason = ERROR_TOO_MANY_RETRIES
94
- self.state = STATE_STOPPED
95
- elif self.state != STATE_STOPPED:
96
- retry_delay = min(2 ** (self.failed_attempts - 1) * 30, 300)
97
- self.failed_attempts += 1
98
- self.state = STATE_DISCONNECTED
99
- await asyncio.sleep(retry_delay)
100
- except Exception as error: # pylint: disable=broad-except
101
- if self.state != STATE_STOPPED:
102
- _LOGGER.error(f"Unexpected exception occurred: {error}")
103
- self._error_reason = ERROR_UNKNOWN
104
- self.state = STATE_STOPPED
105
- else:
106
- if self.state != STATE_STOPPED:
107
- self.state = STATE_DISCONNECTED
108
- await asyncio.sleep(5)
109
-
110
- async def send_str(self, message):
111
- message = str(message)
112
- await self.ws_client.send_str(f'{message}')
113
-
114
- async def listen(self):
115
- """Close the listening websocket."""
116
- self.failed_attempts = 0
117
- while self.state != STATE_STOPPED:
118
- await self.running()
119
-
120
- async def close(self):
121
- """Close the listening websocket."""
122
- self.state = STATE_STOPPED
123
- if self.ws_client:
124
- await self.ws_client.close()