python-homely 0.1.1__py3-none-any.whl → 0.1.2__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.
homely/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Reusable Homely client package extracted from the integration."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.2"
4
4
 
5
5
  from .client import (
6
6
  BASE_URL,
@@ -15,7 +15,7 @@ from .exceptions import (
15
15
  HomelyResponseError,
16
16
  HomelyWebSocketError,
17
17
  )
18
- from .models import TokenResponse
18
+ from .models import TokenEndpointResult, TokenResponse
19
19
  from .websocket import HomelyWebSocket
20
20
 
21
21
  __all__ = [
@@ -29,6 +29,7 @@ __all__ = [
29
29
  "HomelyAuthError",
30
30
  "HomelyResponseError",
31
31
  "HomelyWebSocketError",
32
+ "TokenEndpointResult",
32
33
  "TokenResponse",
33
34
  "auth_header_value",
34
35
  ]
homely/client.py CHANGED
@@ -2,7 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import logging
5
- from typing import Any
5
+ from typing import Any, NoReturn
6
6
 
7
7
  import aiohttp
8
8
 
@@ -11,7 +11,7 @@ from .exceptions import (
11
11
  HomelyConnectionError,
12
12
  HomelyResponseError,
13
13
  )
14
- from .models import TokenResponse
14
+ from .models import TokenEndpointResult, TokenFailureReason, TokenResponse
15
15
 
16
16
  _LOGGER = logging.getLogger(__name__)
17
17
 
@@ -46,6 +46,36 @@ def _response_preview(payload: Any) -> str:
46
46
  return f"{text[:200]}..."
47
47
 
48
48
 
49
+ def _text_preview(text: str | None) -> str | None:
50
+ """Return a trimmed short preview of response text."""
51
+ if text is None:
52
+ return None
53
+
54
+ preview = text.strip()
55
+ if not preview:
56
+ return None
57
+ if len(preview) <= 200:
58
+ return preview
59
+ return f"{preview[:200]}..."
60
+
61
+
62
+ def _error_detail(err: BaseException) -> str:
63
+ """Return a compact detail string for logs and typed results."""
64
+ detail = str(err).strip()
65
+ if detail:
66
+ return detail
67
+ return err.__class__.__name__
68
+
69
+
70
+ def _coarse_token_reason(reason: TokenFailureReason | None) -> str | None:
71
+ """Map detailed token failure reasons to the legacy coarse reason set."""
72
+ if reason == "invalid_auth":
73
+ return "invalid_auth"
74
+ if reason is None:
75
+ return None
76
+ return "cannot_connect"
77
+
78
+
49
79
  class HomelyClient:
50
80
  """Small reusable async client for the Homely cloud API."""
51
81
 
@@ -80,24 +110,17 @@ class HomelyClient:
80
110
 
81
111
  Raises a typed SDK exception on failure.
82
112
  """
83
- response, status = await self._fetch_token_payload(username, password)
84
- if response is not None:
85
- try:
86
- return TokenResponse.from_dict(response)
87
- except (KeyError, TypeError, ValueError) as err:
88
- raise HomelyResponseError(
89
- "Homely authentication response missing required fields",
90
- status=status,
91
- body=_response_preview(response),
92
- ) from err
93
- if status in (400, 401, 403):
94
- raise HomelyAuthError("Invalid Homely username or password")
95
- if status in (200, 201):
96
- raise HomelyResponseError(
97
- "Homely authentication response could not be parsed",
98
- status=status,
99
- )
100
- raise HomelyConnectionError("Could not connect to Homely")
113
+ result = await self.fetch_token_details(username, password)
114
+ if result.token is not None:
115
+ return result.token
116
+ self._raise_token_endpoint_failure(
117
+ result,
118
+ invalid_reason="invalid_auth",
119
+ invalid_message="Invalid Homely username or password",
120
+ connection_message="Could not connect to Homely",
121
+ malformed_message="Homely authentication response could not be parsed",
122
+ response_message="Homely authentication failed",
123
+ )
101
124
 
102
125
  async def _fetch_token_payload(
103
126
  self,
@@ -105,39 +128,25 @@ class HomelyClient:
105
128
  password: str,
106
129
  ) -> tuple[dict[str, Any] | None, int | None]:
107
130
  """Fetch access token payload and include HTTP status when available."""
108
- url = f"{self._base_url}oauth/token"
131
+ result = await self.fetch_token_details(username, password)
132
+ return result.raw, result.status
133
+
134
+ async def fetch_token_details(
135
+ self,
136
+ username: str,
137
+ password: str,
138
+ ) -> TokenEndpointResult:
139
+ """Fetch a token and return a detailed typed result."""
109
140
  payload = {
110
141
  "username": username,
111
142
  "password": password,
112
143
  }
113
-
114
- try:
115
- async with self._session.post(url, json=payload, timeout=self._timeout) as response:
116
- if response.status in (200, 201):
117
- try:
118
- parsed = await response.json()
119
- except (aiohttp.ContentTypeError, TypeError, ValueError) as err:
120
- _LOGGER.debug(
121
- "Token fetch returned invalid JSON status=%s: %s",
122
- response.status,
123
- err,
124
- )
125
- return None, response.status
126
- if not isinstance(parsed, dict):
127
- _LOGGER.debug(
128
- "Token fetch returned unexpected payload type status=%s payload_type=%s",
129
- response.status,
130
- type(parsed).__name__,
131
- )
132
- return None, response.status
133
- _LOGGER.debug("Token fetch successful")
134
- return parsed, response.status
135
-
136
- _LOGGER.debug("Token fetch failed with status=%s", response.status)
137
- return None, response.status
138
- except (aiohttp.ClientError, TimeoutError) as err:
139
- _LOGGER.debug("Token fetch network error: %s", err)
140
- return None, None
144
+ return await self._post_token_endpoint(
145
+ endpoint="oauth/token",
146
+ payload=payload,
147
+ invalid_reason="invalid_auth",
148
+ action="Token fetch",
149
+ )
141
150
 
142
151
  async def fetch_token_with_reason(
143
152
  self,
@@ -145,12 +154,10 @@ class HomelyClient:
145
154
  password: str,
146
155
  ) -> tuple[dict[str, Any] | None, str | None]:
147
156
  """Fetch access token and return optional reason key on failure."""
148
- response, status = await self._fetch_token_payload(username, password)
149
- if response is not None:
150
- return response, None
151
- if status in (400, 401, 403):
152
- return None, "invalid_auth"
153
- return None, "cannot_connect"
157
+ result = await self.fetch_token_details(username, password)
158
+ if result.raw is not None:
159
+ return result.raw, None
160
+ return None, _coarse_token_reason(result.reason)
154
161
 
155
162
  async def fetch_token(
156
163
  self,
@@ -158,71 +165,185 @@ class HomelyClient:
158
165
  password: str,
159
166
  ) -> dict[str, Any] | None:
160
167
  """Fetch access token from API."""
161
- response, _reason = await self.fetch_token_with_reason(username, password)
162
- return response
168
+ result = await self.fetch_token_details(username, password)
169
+ return result.raw
163
170
 
164
171
  async def _fetch_refresh_token_payload(
165
172
  self,
166
173
  refresh_token: str,
167
174
  ) -> tuple[dict[str, Any] | None, int | None]:
168
175
  """Refresh access token payload and include HTTP status when available."""
169
- url = f"{self._base_url}oauth/refresh-token"
176
+ result = await self.fetch_refresh_token_details(refresh_token)
177
+ return result.raw, result.status
178
+
179
+ async def fetch_refresh_token_details(
180
+ self,
181
+ refresh_token: str,
182
+ ) -> TokenEndpointResult:
183
+ """Refresh a token and return a detailed typed result."""
170
184
  payload = {
171
185
  "refresh_token": refresh_token,
172
186
  }
187
+ return await self._post_token_endpoint(
188
+ endpoint="oauth/refresh-token",
189
+ payload=payload,
190
+ invalid_reason="invalid_refresh_token",
191
+ action="Token refresh",
192
+ )
193
+
194
+ async def fetch_refresh_token(self, refresh_token: str) -> dict[str, Any] | None:
195
+ """Refresh access token using refresh token."""
196
+ result = await self.fetch_refresh_token_details(refresh_token)
197
+ return result.raw
198
+
199
+ async def refresh_access_token(self, refresh_token: str) -> TokenResponse:
200
+ """Refresh access token and return a typed token response."""
201
+ result = await self.fetch_refresh_token_details(refresh_token)
202
+ if result.token is not None:
203
+ return result.token
204
+ self._raise_token_endpoint_failure(
205
+ result,
206
+ invalid_reason="invalid_refresh_token",
207
+ invalid_message="Homely rejected the supplied refresh token",
208
+ connection_message="Could not refresh Homely access token",
209
+ malformed_message="Homely refresh response could not be parsed",
210
+ response_message="Homely refresh request failed",
211
+ )
212
+
213
+ async def _post_token_endpoint(
214
+ self,
215
+ *,
216
+ endpoint: str,
217
+ payload: dict[str, Any],
218
+ invalid_reason: TokenFailureReason,
219
+ action: str,
220
+ ) -> TokenEndpointResult:
221
+ """Post to a token endpoint and return a detailed typed result."""
222
+ url = f"{self._base_url}{endpoint}"
173
223
 
174
224
  try:
175
225
  async with self._session.post(url, json=payload, timeout=self._timeout) as response:
226
+ body_preview = await self._response_text_preview(response)
176
227
  if response.status in (200, 201):
177
228
  try:
178
229
  parsed = await response.json()
179
230
  except (aiohttp.ContentTypeError, TypeError, ValueError) as err:
231
+ detail = _error_detail(err)
180
232
  _LOGGER.debug(
181
- "Token refresh returned invalid JSON status=%s: %s",
233
+ "%s returned invalid JSON status=%s: %s",
234
+ action,
182
235
  response.status,
183
- err,
236
+ detail,
237
+ )
238
+ return TokenEndpointResult(
239
+ reason="invalid_json",
240
+ status=response.status,
241
+ detail=detail,
242
+ body_preview=body_preview,
184
243
  )
185
- return None, response.status
186
244
  if not isinstance(parsed, dict):
245
+ detail = f"payload_type={type(parsed).__name__}"
187
246
  _LOGGER.debug(
188
- "Token refresh returned unexpected payload type status=%s payload_type=%s",
247
+ "%s returned unexpected payload type status=%s payload_type=%s",
248
+ action,
189
249
  response.status,
190
250
  type(parsed).__name__,
191
251
  )
192
- return None, response.status
193
- _LOGGER.debug("Token refresh successful")
194
- return parsed, response.status
195
- _LOGGER.debug("Token refresh failed with status=%s", response.status)
196
- return None, response.status
197
- except (aiohttp.ClientError, TimeoutError) as err:
198
- _LOGGER.debug("Token refresh network error: %s", err)
199
- return None, None
200
-
201
- async def fetch_refresh_token(self, refresh_token: str) -> dict[str, Any] | None:
202
- """Refresh access token using refresh token."""
203
- response, _status = await self._fetch_refresh_token_payload(refresh_token)
204
- return response
252
+ return TokenEndpointResult(
253
+ reason="invalid_payload",
254
+ status=response.status,
255
+ detail=detail,
256
+ body_preview=_response_preview(parsed),
257
+ )
258
+ if not parsed:
259
+ _LOGGER.debug(
260
+ "%s returned an empty payload status=%s",
261
+ action,
262
+ response.status,
263
+ )
264
+ return TokenEndpointResult(
265
+ reason="empty_response",
266
+ status=response.status,
267
+ body_preview=_response_preview(parsed),
268
+ )
269
+ try:
270
+ token = TokenResponse.from_dict(parsed)
271
+ except (KeyError, TypeError, ValueError) as err:
272
+ detail = _error_detail(err)
273
+ _LOGGER.debug(
274
+ "%s returned malformed payload status=%s: %s",
275
+ action,
276
+ response.status,
277
+ detail,
278
+ )
279
+ return TokenEndpointResult(
280
+ reason="invalid_payload",
281
+ status=response.status,
282
+ detail=detail,
283
+ body_preview=_response_preview(parsed),
284
+ )
285
+ _LOGGER.debug("%s successful", action)
286
+ return TokenEndpointResult(token=token, status=response.status)
287
+
288
+ if response.status in (400, 401, 403):
289
+ _LOGGER.debug("%s rejected with status=%s", action, response.status)
290
+ return TokenEndpointResult(
291
+ reason=invalid_reason,
292
+ status=response.status,
293
+ body_preview=body_preview,
294
+ )
295
+
296
+ _LOGGER.debug("%s failed with status=%s", action, response.status)
297
+ return TokenEndpointResult(
298
+ reason="http_error",
299
+ status=response.status,
300
+ body_preview=body_preview,
301
+ )
302
+ except TimeoutError as err:
303
+ detail = _error_detail(err)
304
+ _LOGGER.debug("%s timeout: %s", action, detail)
305
+ return TokenEndpointResult(reason="timeout", detail=detail)
306
+ except aiohttp.ClientError as err:
307
+ detail = _error_detail(err)
308
+ _LOGGER.debug("%s network error: %s", action, detail)
309
+ return TokenEndpointResult(reason="network_error", detail=detail)
310
+
311
+ async def _response_text_preview(self, response: aiohttp.ClientResponse) -> str | None:
312
+ """Read and shorten response text for diagnostics when available."""
313
+ try:
314
+ return _text_preview(await response.text())
315
+ except (TypeError, ValueError, UnicodeDecodeError) as err:
316
+ _LOGGER.debug("Could not read response text status=%s: %s", response.status, err)
317
+ return None
205
318
 
206
- async def refresh_access_token(self, refresh_token: str) -> TokenResponse:
207
- """Refresh access token and return a typed token response."""
208
- response, status = await self._fetch_refresh_token_payload(refresh_token)
209
- if response is not None:
210
- try:
211
- return TokenResponse.from_dict(response)
212
- except (KeyError, TypeError, ValueError) as err:
213
- raise HomelyResponseError(
214
- "Homely refresh response missing required fields",
215
- status=status,
216
- body=_response_preview(response),
217
- ) from err
218
- if status in (400, 401, 403):
219
- raise HomelyAuthError("Homely rejected the supplied refresh token")
220
- if status in (200, 201):
319
+ def _raise_token_endpoint_failure(
320
+ self,
321
+ result: TokenEndpointResult,
322
+ *,
323
+ invalid_reason: TokenFailureReason,
324
+ invalid_message: str,
325
+ connection_message: str,
326
+ malformed_message: str,
327
+ response_message: str,
328
+ ) -> NoReturn:
329
+ """Raise a typed SDK exception for a failed token endpoint result."""
330
+ if result.reason == invalid_reason:
331
+ raise HomelyAuthError(invalid_message)
332
+ if result.reason in ("timeout", "network_error"):
333
+ raise HomelyConnectionError(connection_message)
334
+ if result.reason in ("invalid_json", "invalid_payload", "empty_response"):
221
335
  raise HomelyResponseError(
222
- "Homely refresh response could not be parsed",
223
- status=status,
336
+ malformed_message,
337
+ status=result.status,
338
+ body=result.body_preview,
339
+ body_preview=result.body_preview,
224
340
  )
225
- raise HomelyConnectionError("Could not refresh Homely access token")
341
+ raise HomelyResponseError(
342
+ response_message,
343
+ status=result.status,
344
+ body=result.body_preview,
345
+ body_preview=result.body_preview,
346
+ )
226
347
 
227
348
  async def _get_locations_payload(
228
349
  self,
@@ -246,7 +367,8 @@ class HomelyClient:
246
367
  return None, response.status
247
368
  if not isinstance(parsed, list):
248
369
  _LOGGER.debug(
249
- "Locations fetch returned unexpected payload type status=%s payload_type=%s",
370
+ "Locations fetch returned unexpected payload type "
371
+ "status=%s payload_type=%s",
250
372
  response.status,
251
373
  type(parsed).__name__,
252
374
  )
@@ -303,7 +425,8 @@ class HomelyClient:
303
425
  parsed = await response.json()
304
426
  except (aiohttp.ContentTypeError, TypeError, ValueError) as err:
305
427
  _LOGGER.debug(
306
- "Location data fetch returned invalid JSON status=%s location_id=%s: %s",
428
+ "Location data fetch returned invalid JSON "
429
+ "status=%s location_id=%s: %s",
307
430
  response.status,
308
431
  _log_identifier(location_id),
309
432
  err,
@@ -311,7 +434,8 @@ class HomelyClient:
311
434
  return None, response.status
312
435
  if not isinstance(parsed, dict):
313
436
  _LOGGER.debug(
314
- "Location data fetch returned unexpected payload type status=%s location_id=%s payload_type=%s",
437
+ "Location data fetch returned unexpected payload "
438
+ "type status=%s location_id=%s payload_type=%s",
315
439
  response.status,
316
440
  _log_identifier(location_id),
317
441
  type(parsed).__name__,
homely/exceptions.py CHANGED
@@ -23,10 +23,14 @@ class HomelyResponseError(HomelyError):
23
23
  *,
24
24
  status: int | None = None,
25
25
  body: str | None = None,
26
+ body_preview: str | None = None,
26
27
  ) -> None:
27
28
  """Initialize the exception with optional response details."""
28
29
  super().__init__(message)
29
30
  self.status = status
31
+ if body_preview is None:
32
+ body_preview = body
33
+ self.body_preview = body_preview
30
34
  self.body = body
31
35
 
32
36
 
homely/models.py CHANGED
@@ -2,7 +2,18 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from dataclasses import dataclass
5
- from typing import Any
5
+ from typing import Any, Literal
6
+
7
+ TokenFailureReason = Literal[
8
+ "invalid_auth",
9
+ "invalid_refresh_token",
10
+ "http_error",
11
+ "network_error",
12
+ "timeout",
13
+ "invalid_json",
14
+ "invalid_payload",
15
+ "empty_response",
16
+ ]
6
17
 
7
18
 
8
19
  @dataclass(slots=True)
@@ -29,3 +40,26 @@ class TokenResponse:
29
40
  expires_in=parsed_expires_in,
30
41
  raw=dict(data),
31
42
  )
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class TokenEndpointResult:
47
+ """Detailed result returned by authentication and refresh helpers."""
48
+
49
+ token: TokenResponse | None = None
50
+ reason: TokenFailureReason | None = None
51
+ status: int | None = None
52
+ detail: str | None = None
53
+ body_preview: str | None = None
54
+
55
+ @property
56
+ def ok(self) -> bool:
57
+ """Return whether the request produced a usable token response."""
58
+ return self.token is not None
59
+
60
+ @property
61
+ def raw(self) -> dict[str, Any] | None:
62
+ """Return the raw token payload when available."""
63
+ if self.token is None:
64
+ return None
65
+ return self.token.raw
homely/py.typed ADDED
@@ -0,0 +1 @@
1
+
homely/websocket.py CHANGED
@@ -45,9 +45,9 @@ class HomelyWebSocket:
45
45
  self.location_id = location_id
46
46
  self.token = token
47
47
  self.on_data_update = on_data_update
48
- self.socket = None
48
+ self.socket: Any | None = None
49
49
  self._is_closing = False
50
- self._reconnect_task: asyncio.Task | None = None
50
+ self._reconnect_task: asyncio.Task[None] | None = None
51
51
  self._reconnect_interval = 300
52
52
  self._reconnect_warn_every = 12
53
53
  self._status_update_callback = status_update_callback
@@ -268,7 +268,7 @@ class HomelyWebSocket:
268
268
  return False
269
269
 
270
270
  try:
271
- import socketio
271
+ import socketio # type: ignore[import-untyped]
272
272
  except ImportError:
273
273
  _LOGGER.error("python-socketio is not installed. WebSocket disabled %s.", self._ctx())
274
274
  self._set_status("Disconnected", "socketio missing")
@@ -293,35 +293,36 @@ class HomelyWebSocket:
293
293
  engineio_logger=False,
294
294
  )
295
295
 
296
- @self.socket.event
297
- async def connect():
296
+ async def connect() -> None:
298
297
  self._on_connect()
299
298
 
300
- @self.socket.event
301
- async def disconnect(*args):
299
+ async def disconnect(*args: Any) -> None:
302
300
  self._on_disconnect(self._build_reason(args[0] if args else None))
303
301
 
304
- @self.socket.event
305
- async def message(data):
302
+ async def message(data: Any) -> None:
306
303
  self._on_event(data)
307
304
 
308
- @self.socket.event
309
- async def event(data):
305
+ async def event(data: Any) -> None:
310
306
  self._on_event(data)
311
307
 
312
- @self.socket.on("*")
313
- async def catch_all(event, data):
308
+ async def catch_all(event: str, data: Any) -> None:
314
309
  if event not in ("connect", "disconnect", "message", "event", "connect_error"):
315
310
  _LOGGER.debug("WebSocket event %s type=%s", self._ctx(), event)
316
311
  self._on_event({"type": event, "payload": data})
317
312
 
318
- @self.socket.event
319
- async def connect_error(data):
313
+ async def connect_error(data: Any) -> None:
320
314
  raw_reason = self._build_reason(data)
321
315
  reason = f"connect_error: {raw_reason}" if raw_reason else "connect_error"
322
316
  _LOGGER.debug("WebSocket connect_error %s: %s", self._ctx(), reason)
323
317
  self._on_disconnect(reason)
324
318
 
319
+ self.socket.on("connect", connect)
320
+ self.socket.on("disconnect", disconnect)
321
+ self.socket.on("message", message)
322
+ self.socket.on("event", event)
323
+ self.socket.on("*", catch_all)
324
+ self.socket.on("connect_error", connect_error)
325
+
325
326
  bearer_token = self._bearer_value(self.token)
326
327
  query = urlencode(
327
328
  {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-homely
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Async Python client for the Homely cloud API, built for Home Assistant but usable anywhere.
5
5
  Author: Ludvik Blichfeldt Rød
6
6
  License-Expression: MIT
@@ -102,14 +102,23 @@ async def main() -> None:
102
102
 
103
103
  - `authenticate(username, password) -> TokenResponse`
104
104
  - `refresh_access_token(refresh_token) -> TokenResponse`
105
+ - `fetch_token_details(username, password) -> TokenEndpointResult`
106
+ - `fetch_refresh_token_details(refresh_token) -> TokenEndpointResult`
105
107
  - `get_locations_or_raise(token) -> list[dict]`
106
108
  - `get_home_data_or_raise(token, location_id) -> dict`
107
109
  - `HomelyWebSocket(...).connect_or_raise()`
108
110
 
111
+ Legacy compatibility helpers remain available:
112
+
113
+ - `fetch_token_with_reason(username, password) -> tuple[dict | None, str | None]`
114
+ - `fetch_token(username, password) -> dict | None`
115
+ - `fetch_refresh_token(refresh_token) -> dict | None`
116
+
109
117
  Main exports:
110
118
 
111
119
  - `HomelyClient`
112
120
  - `HomelyWebSocket`
121
+ - `TokenEndpointResult`
113
122
  - `TokenResponse`
114
123
  - `HomelyConnectionError`
115
124
  - `HomelyAuthError`
@@ -121,12 +130,33 @@ Main exports:
121
130
  - `HomelyConnectionError`: network or service unavailable
122
131
  - `HomelyAuthError`: invalid credentials or rejected token
123
132
  - `HomelyResponseError`: unexpected response or HTTP failure
133
+ Carries `status` and `body_preview` when available.
124
134
  - `HomelyWebSocketError`: websocket could not be established
125
135
 
136
+ ## Token Diagnostics
137
+
138
+ If you need more than success/failure, use the detailed token helpers:
139
+
140
+ ```python
141
+ result = await client.fetch_refresh_token_details(refresh_token)
142
+ if result.ok:
143
+ print(result.token.access_token)
144
+ else:
145
+ print(result.reason, result.status, result.detail, result.body_preview)
146
+ ```
147
+
148
+ `TokenEndpointResult.reason` can distinguish between invalid credentials, invalid refresh
149
+ tokens, network errors, timeouts, malformed JSON, malformed payloads, empty responses, and
150
+ unexpected HTTP failures.
151
+
152
+ ## Websocket Note
153
+
154
+ Refreshing an access token does not require forcing an already-connected websocket to reconnect.
155
+ The websocket token only matters when a new websocket connection is established or re-established.
156
+
126
157
  ## License
127
158
 
128
159
  MIT. See [LICENSE](LICENSE).
129
160
 
130
161
 
131
- ⭐ If you find this integration useful, please consider giving it a star on [GitHub](https://github.com/ludvikroed/python-homely)! ⭐
132
-
162
+ ⭐ If you find this package useful, please consider giving it a star on [GitHub](https://github.com/ludvikroed/python-homely)! ⭐
@@ -0,0 +1,11 @@
1
+ homely/__init__.py,sha256=PlZPdFw1D9L2lBeNmdKAsYGgLHjjwRP3XAwmYBRo2Is,746
2
+ homely/client.py,sha256=iqM1ChFEVez5PJcvWk8BlkkVx4bekaQacBa-rAvSxpo,18276
3
+ homely/exceptions.py,sha256=w6y5MC8yjPe5QtI-YEKUsbAPF1D0ARWx6ZkRsLTjrgQ,1040
4
+ homely/models.py,sha256=8chNpjo1SF4TGww7XPWFUK3kXILH3bSmUnjNteyfsys,1842
5
+ homely/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
+ homely/websocket.py,sha256=g6WJxvEjt-pjWkEk3qyBdhs6OCX3l-hGBOJ3dcPxHCI,15538
7
+ python_homely-0.1.2.dist-info/licenses/LICENSE,sha256=nNVHKvQjryAonWsz5TbhzLd6M2kZaoNUvhxz92MgeAA,1079
8
+ python_homely-0.1.2.dist-info/METADATA,sha256=piiYKUni3USyn2FNt6V8qvVCwDOk6aCkBKAesK7a78Q,5239
9
+ python_homely-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ python_homely-0.1.2.dist-info/top_level.txt,sha256=auE-j6ghVMdT_jAw04jiXthgbuLpi-jYBU0fCkKvREQ,7
11
+ python_homely-0.1.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- homely/__init__.py,sha256=9S-ApRtXQuVFQhNSN5oymZqHZcHu6MGO8anHiXFVf9c,698
2
- homely/client.py,sha256=Cvdy490yykAXRGy8JR9lhrB96Ka73bnWgrYxlhw-LYo,13849
3
- homely/exceptions.py,sha256=g1v7QR9uDydzDCH12FD-3pZN2MGPJu1lDgAnNdT0lCA,893
4
- homely/models.py,sha256=NgNwYeOo4UiiyP29iFM0rN7k2HIkxZg4s5DivgfdYKM,968
5
- homely/websocket.py,sha256=l5W5JB0aiMZy1H41uYcNzWCKgEmRXGicSa-05VAkfAI,15305
6
- python_homely-0.1.1.dist-info/licenses/LICENSE,sha256=nNVHKvQjryAonWsz5TbhzLd6M2kZaoNUvhxz92MgeAA,1079
7
- python_homely-0.1.1.dist-info/METADATA,sha256=A_jTgr71SLf9_-icc49TVlRM2wh2IIugR1exuPDYatM,4062
8
- python_homely-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
- python_homely-0.1.1.dist-info/top_level.txt,sha256=auE-j6ghVMdT_jAw04jiXthgbuLpi-jYBU0fCkKvREQ,7
10
- python_homely-0.1.1.dist-info/RECORD,,