python-swidget 0.0.32__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-swidget
3
- Version: 0.0.32
3
+ Version: 1.0.0
4
4
  Summary: Python API for Swidget smart devices
5
5
  Home-page: https://github.com/swidget/python-swidget
6
6
  License: GPL-3.0-or-later
@@ -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.0"
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)
@@ -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()
@@ -39,7 +39,10 @@ class SwidgetProtocol(ssdp.SimpleServiceDiscoveryProtocol):
39
39
  insert_type = headers["SERVER"].split(" ")[1].split("+")[1].split("/")[0]
40
40
  friendly_name = headers["SERVER"].split("/")[2].strip('"')
41
41
  devices[mac_address] = SwidgetDiscoveredDevice(mac_address, ip_address, friendly_name)
42
+ <<<<<<< HEAD
43
+ =======
42
44
  _LOGGER.debug(f"Swidget device '{friendly_name}' at {ip_address}")
45
+ >>>>>>> 690a0c560a8b2ff39245d1a8354ced968d79e5de
43
46
 
44
47
 
45
48
  async def discover_devices(timeout=RESPONSE_SEC):
@@ -65,21 +68,26 @@ async def discover_devices(timeout=RESPONSE_SEC):
65
68
  return devices
66
69
 
67
70
 
68
- async def discover_single(host: str, token_name: str, password: str, ssl: bool, use_websockets: bool) -> SwidgetDevice:
71
+ async def discover_single(host: str, token_name: str, password: str, use_https: bool, use_websockets: bool) -> SwidgetDevice:
69
72
  """Discover a single device by the given IP address.
70
73
 
71
74
  :param host: Hostname of device to query
72
75
  :rtype: SwidgetDevice
73
76
  :return: Object for querying/controlling found device.
74
77
  """
75
- swidget_device = SwidgetDevice(host, token_name, password, ssl, use_websockets)
78
+ _LOGGER.debug(f"Checking for device at {host}")
79
+ swidget_device = SwidgetDevice(host, token_name, password, use_https, use_websockets=False)
80
+ _LOGGER.debug(f"Asking {host} for summary data")
76
81
  await swidget_device.get_summary()
77
82
  device_type = swidget_device.device_type
83
+ _LOGGER.debug(f"{host} is of type {device_type}")
78
84
  await swidget_device.stop()
79
85
 
86
+ _LOGGER.debug(f"Creating new device class of type: {device_type}")
80
87
  device_class = _get_device_class(device_type)
81
- dev = device_class(host, token_name, password, ssl, use_websockets)
82
- await dev.update()
88
+ _LOGGER.debug(f"{device_class}")
89
+ dev = device_class(host, token_name, password, use_https, use_websockets)
90
+ await dev.start()
83
91
  return dev
84
92
 
85
93
 
@@ -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,19 @@ 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
+ headers = {self.token_name: self.secret_key,
38
+ 'Connection': 'keep-alive'}
39
+ connector = TCPConnector(verify_ssl=False, force_close=True)
37
40
  self._session = ClientSession(headers=headers, connector=connector)
38
41
  self._last_update = None
39
42
  if self.use_websockets:
@@ -45,37 +48,62 @@ class SwidgetDevice:
45
48
  session=self._session)
46
49
 
47
50
  def get_websocket(self):
48
- return self._websocket
51
+ if self.use_websockets:
52
+ return self._websocket
53
+ return None
49
54
 
50
55
  def set_countdown_timer(self, minutes):
51
56
  raise NotImplementedError()
52
57
 
58
+ async def connect(self):
59
+ await self._websocket.connect()
60
+
61
+ async def start(self):
62
+ """Start the websocket."""
63
+ _LOGGER.debug("SwidgetDevice.start()")
64
+ if self.use_websockets:
65
+ _LOGGER.debug("Calling self._websocket.connect()")
66
+ await self._websocket.connect()
67
+ _LOGGER.debug("Calling self._websocket.listen() ")
68
+ asyncio.create_task(self._websocket.listen())
69
+
53
70
  async def stop(self):
54
71
  """Stop the websocket."""
72
+ _LOGGER.debug("SwidgetDevice.stop()")
55
73
  if hasattr(self, '_websocket'):
56
74
  await self._websocket.close()
57
75
  await self._session.close()
58
76
 
59
77
  async def message_callback(self, message):
60
78
  """Entrypoint for a websocket callback"""
79
+ _LOGGER.debug("SwidgetDevice.message_callback() called")
61
80
  if message["request_id"] == "summary":
81
+ _LOGGER.debug("Calling SwidgetDevice.process_summary()")
62
82
  await self.process_summary(message)
63
83
  elif message["request_id"] == "state" or message["request_id"] == "DYNAMIC_UPDATE" or message["request_id"] == "command":
84
+ _LOGGER.debug("Calling SwidgetDevice.process_state()")
64
85
  await self.process_state(message)
86
+ else:
87
+ _LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
65
88
 
66
89
  async def get_summary(self):
67
90
  """Get a summary of the device over HTTP"""
68
- try:
91
+ _LOGGER.debug("SwidgetDevice.get_summary() called")
92
+ if self.use_websockets:
93
+ _LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
94
+ await self._websocket.send_str(json.dumps({"type": "summary", "request_id": "summary"}))
95
+ else:
96
+ _LOGGER.debug("In http mode. Sending get_summary() command over http")
69
97
  async with self._session.get(
70
- url=f"https://{self.ip_address}/api/v1/summary", ssl=self.ssl
98
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
71
99
  ) as response:
72
100
  summary = await response.json()
73
101
  await self.process_summary(summary)
74
- except:
75
- raise SwidgetException("Unable to connect to device")
76
102
 
77
103
  async def process_summary(self, summary):
78
104
  """ Process the data around the summary of the device"""
105
+ _LOGGER.debug("SwidgetDevice.process_summary() called")
106
+ _LOGGER.debug(f"Summary to process: {summary}")
79
107
  self.model = summary["model"]
80
108
  self.mac_address = summary["mac"]
81
109
  self.version = summary["version"]
@@ -89,9 +117,10 @@ class SwidgetDevice:
89
117
  self._last_update = int(time.time())
90
118
 
91
119
  async def get_friendly_name(self):
120
+ _LOGGER.debug("SwidgetDevice.get_friendly_name() called")
92
121
  try:
93
122
  async with self._session.get(
94
- url=f"https://{self.ip_address}/api/v1/name", ssl=self.ssl
123
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
95
124
  ) as response:
96
125
  name = await response.json()
97
126
  except Exception:
@@ -99,24 +128,33 @@ class SwidgetDevice:
99
128
  await self.process_friendly_name(name['name'])
100
129
 
101
130
  async def process_friendly_name(self, name):
131
+ _LOGGER.debug("SwidgetDevice.process_friendly_name() called")
102
132
  self._friendly_name = name
133
+ self._last_update = int(time.time())
103
134
 
104
135
  async def get_state(self):
105
136
  """ 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)
137
+ _LOGGER.debug("SwidgetDevice.get_state() called")
138
+ if self.use_websockets:
139
+ _LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
140
+ await self._websocket.send_str(json.dumps({"type": "state", "request_id": "state"}))
141
+ else:
142
+ _LOGGER.debug("In http mode. Sending get_summary() command over http")
143
+ async with self._session.get(
144
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/state", ssl=False
145
+ ) as response:
146
+ state = await response.json()
147
+ await self.process_state(state)
111
148
 
112
149
  async def process_state(self, state):
113
150
  """ Process any information about the state of the device or insert"""
114
151
  # State is not always in the state (during callback)
152
+ _LOGGER.debug("SwidgetDevice.process_state() called")
153
+ _LOGGER.debug(f"State to process: {state}")
115
154
  try:
116
155
  self.rssi = state["connection"]["rssi"]
117
156
  except:
118
157
  pass
119
-
120
158
  for assembly in self.assemblies:
121
159
  for id, component in self.assemblies[assembly].components.items():
122
160
  try:
@@ -126,37 +164,49 @@ class SwidgetDevice:
126
164
  self._last_update = int(time.time())
127
165
 
128
166
  async def update(self):
167
+ _LOGGER.debug("SwidgetDevice.update() called")
129
168
  if self._last_update is None:
130
169
  _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()
170
+ await self.get_summary()
171
+ await self.get_state()
172
+ if self._friendly_name == "Unknown Swidget Device":
173
+ await self.get_friendly_name()
174
+ elif (int(time.time()) - self._last_update) < 5:
175
+ _LOGGER.debug("update() recently called, not executing")
176
+ else:
177
+ _LOGGER.debug("Requesting an update of the device")
178
+ await self.get_summary()
179
+ await self.get_state()
135
180
 
136
181
  async def send_config(self, payload: dict):
137
- data = json.dumps({"type":"config","request_id":"abcd", "payload": payload})
182
+ _LOGGER.debug("SwidgetDevice.send_config() called")
183
+ data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
138
184
  await self._websocket.send_str(data)
139
185
 
140
186
  async def send_command(
141
187
  self, assembly: str, component: str, function: str, command: dict
142
188
  ):
189
+ _LOGGER.debug("SwidgetDevice.send_command() called")
143
190
  """Send a command to the Swidget device either using a HTTP call or the existing websocket"""
144
191
  data = {assembly: {"components": {component: {function: command}}}}
145
-
192
+ _LOGGER.debug(f"Command to send: {data}")
146
193
  if self.use_websockets:
194
+ _LOGGER.debug("In websocket mode. Sending command over websocket")
147
195
  data = json.dumps({"type": "command",
148
196
  "request_id": "command",
149
197
  "payload": data
150
198
  })
151
199
  await self._websocket.send_str(data)
152
200
  else:
201
+ _LOGGER.debug("NOT in websocket mode, sending command over HTTP")
153
202
  async with self._session.post(
154
- url=f"https://{self.ip_address}/api/v1/command",
155
- ssl=self.ssl,
203
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/command",
204
+ ssl=False,
156
205
  data=json.dumps(data),
157
206
  ) as response:
158
207
  state = await response.json()
159
208
 
209
+ # Do a hard set of the new state of the device. May change this in the future
160
210
  function_value = state[assembly]["components"][component][function]
161
211
  self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
162
212
 
@@ -165,12 +215,13 @@ class SwidgetDevice:
165
215
 
166
216
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
167
217
  """
218
+ _LOGGER.debug("SwidgetDevice.ping() called")
168
219
  try:
169
220
  async with self._session.get(
170
- url=f"https://{self.ip_address}/ping",
171
- ssl=self.ssl
221
+ url=f"{self.uri_scheme}://{self.ip_address}/ping",
222
+ ssl=False
172
223
  ) as response:
173
- return response.text
224
+ return response.status
174
225
  except:
175
226
  raise SwidgetException
176
227
 
@@ -179,12 +230,13 @@ class SwidgetDevice:
179
230
 
180
231
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
181
232
  """
233
+ _LOGGER.debug("SwidgetDevice.blink() called")
182
234
  try:
183
235
  async with self._session.get(
184
- url=f"https://{self.ip_address}/blink",
185
- ssl=self.ssl
236
+ url=f"{self.uri_scheme}://{self.ip_address}/blink",
237
+ ssl=False
186
238
  ) as response:
187
- return response.text
239
+ return await response.json()
188
240
  except:
189
241
  raise SwidgetException
190
242
 
@@ -193,12 +245,13 @@ class SwidgetDevice:
193
245
 
194
246
  :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
195
247
  """
248
+ _LOGGER.debug("SwidgetDevice.enable_debug_server() called")
196
249
  try:
197
250
  async with self._session.get(
198
- url=f"https://{self.ip_address}/debug?x-secret-key={self.secret_key}",
199
- ssl=self.ssl
251
+ url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
252
+ ssl=False
200
253
  ) as response:
201
- return response.text
254
+ return await response.json()
202
255
  except:
203
256
  raise SwidgetException
204
257
 
@@ -210,10 +263,43 @@ class SwidgetDevice:
210
263
  try:
211
264
 
212
265
  async with self._session.delete(
213
- url=f"https://{self.ip_address}/api/v1/reset",
214
- ssl=self.ssl
266
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset",
267
+ ssl=False
215
268
  ) as response:
216
- return response.text
269
+ return await response.json()
270
+ except:
271
+ raise SwidgetException
272
+
273
+ async def check_for_updates(self):
274
+ """Tell the device to contact the Swidget servers to see if there is an available update
275
+
276
+ :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
277
+ """
278
+ try:
279
+
280
+ async with self._session.get(
281
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
282
+ ssl=False
283
+ ) as response:
284
+ return await response.json()
285
+ except:
286
+ raise SwidgetException
287
+
288
+ async def update_version(self, version):
289
+ """Tell the device to download and apply an update
290
+
291
+ :raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
292
+ """
293
+ try:
294
+ data = {
295
+ "version": version
296
+ }
297
+ async with self._session.post(
298
+ url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
299
+ ssl=False,
300
+ data=json.dumps(data)
301
+ ) as response:
302
+ return await response
217
303
  except:
218
304
  raise SwidgetException
219
305
 
@@ -329,7 +415,7 @@ class SwidgetDevice:
329
415
  @property
330
416
  def is_dimmable(self) -> bool:
331
417
  """Return True if the device is dimmable."""
332
- return False
418
+ return self.is_dimmer
333
419
 
334
420
  @property # type: ignore
335
421
  def friendly_name(self) -> str:
@@ -346,11 +432,14 @@ class SwidgetDevice:
346
432
 
347
433
  async def turn_on(self):
348
434
  """Turn the device on."""
435
+ _LOGGER.debug("SwidgetDevice.turn_on() called")
349
436
  await self.send_command(
350
437
  assembly="host", component="0", function="toggle", command={"state": "on"}
351
438
  )
439
+
352
440
  async def turn_off(self):
353
441
  """Turn the device off."""
442
+ _LOGGER.debug("SwidgetDevice.turn_off() called")
354
443
  await self.send_command(
355
444
  assembly="host", component="0", function="toggle", command={"state": "off"}
356
445
  )
@@ -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,99 @@
1
+ import aiohttp
2
+ import logging
3
+ import socket
4
+
5
+ _LOGGER = logging.getLogger(__name__)
6
+
7
+ class SwidgetWebsocket:
8
+ """A websocket connection to a Swidget Device"""
9
+
10
+ # pylint: disable=too-many-instance-attributes
11
+ _client: aiohttp.ClientWebSocketResponse | None = None
12
+
13
+ def __init__(
14
+ self,
15
+ host,
16
+ token_name,
17
+ secret_key,
18
+ callback,
19
+ session=None,
20
+ use_security=True,
21
+ ):
22
+
23
+ self.session = session or aiohttp.ClientSession()
24
+ self.use_security = use_security
25
+ self.uri = self.get_uri(host, token_name, secret_key)
26
+ self.callback = callback
27
+ self._verify_ssl = False
28
+ self._state = None
29
+ self.failed_attempts = 0
30
+ self._error_reason = None
31
+ self.headers = {'Connection': 'Upgrade'}
32
+
33
+ @property
34
+ def connected(self) -> bool:
35
+ return self._client is not None and not self._client.closed
36
+
37
+ def get_uri(self, host, token_name, secret_key):
38
+ """Generate the websocket URI"""
39
+ if self.use_security:
40
+ return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
41
+ else:
42
+ return f"ws://{host}/api/v1/sock?{token_name}={secret_key}"
43
+
44
+ async def connect(self) -> None:
45
+ _LOGGER.debug("websocket.connect() called")
46
+ if self.connected:
47
+ return
48
+
49
+ if not self.session:
50
+ raise
51
+
52
+ try:
53
+ self._client = await self.session.ws_connect(url=self.uri, headers=self.headers, verify_ssl=self._verify_ssl, heartbeat=30)
54
+ except (
55
+ aiohttp.WSServerHandshakeError,
56
+ aiohttp.ClientConnectionError,
57
+ socket.gaierror,
58
+ ) as exception:
59
+ msg = (
60
+ "Error occurred while communicating with WLED device"
61
+ f" on WebSocket at {self.host}"
62
+ )
63
+ raise
64
+
65
+ async def close(self) -> None:
66
+ _LOGGER.debug("websocket.close() called")
67
+ if not self._client or not self.connected:
68
+ return
69
+ await self._client.close()
70
+
71
+ async def send_str(self, message):
72
+ """Send a message through the websocket."""
73
+ _LOGGER.debug("websocket.send_str() called")
74
+ message = str(message)
75
+ _LOGGER.debug(f"Sending messsage over websocket: {message}")
76
+ await self._client.send_str(f'{message}')
77
+
78
+ async def listen(self):
79
+ _LOGGER.debug("websocket.listen() called")
80
+ if not self._client or not self.connected:
81
+ raise
82
+
83
+ while not self._client.closed:
84
+ message = await self._client.receive()
85
+
86
+ if message.type == aiohttp.WSMsgType.ERROR:
87
+ raise
88
+
89
+ if message.type == aiohttp.WSMsgType.TEXT:
90
+ message_data = message.json()
91
+ _LOGGER.debug(f"Data received from websocket: {message_data}")
92
+ await self.callback(message_data)
93
+
94
+ if message.type in (
95
+ aiohttp.WSMsgType.CLOSE,
96
+ aiohttp.WSMsgType.CLOSED,
97
+ aiohttp.WSMsgType.CLOSING,
98
+ ):
99
+ _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()