lghorizon 0.9.0.dev3__py3-none-any.whl → 0.9.1__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.
@@ -1,9 +1,10 @@
1
1
  import asyncio
2
2
  import json
3
3
  import logging
4
+ from typing import Any, Callable, Coroutine
4
5
 
5
6
  import paho.mqtt.client as mqtt
6
- from typing import Any, Callable, Coroutine
7
+
7
8
  from .helpers import make_id
8
9
  from .lghorizon_models import LGHorizonAuth
9
10
 
@@ -11,32 +12,46 @@ _logger = logging.getLogger(__name__)
11
12
 
12
13
 
13
14
  class LGHorizonMqttClient:
14
- """LGHorizon MQTT client."""
15
-
16
- _mqtt_broker_url: str = ""
17
- _mqtt_client: mqtt.Client
18
- _auth: LGHorizonAuth
19
- _mqtt_token: str = ""
20
- client_id: str = ""
21
- _on_connected_callback: Callable[[], Coroutine[Any, Any, Any]]
22
- _on_message_callback: Callable[[dict, str], Coroutine[Any, Any, Any]]
23
-
24
- @property
25
- def is_connected(self):
26
- """Is client connected."""
27
- return self._mqtt_client.is_connected
15
+ """Asynchronous-friendly wrapper around Paho MQTT."""
28
16
 
29
17
  def __init__(
30
18
  self,
31
19
  auth: LGHorizonAuth,
32
20
  on_connected_callback: Callable[[], Coroutine[Any, Any, Any]],
33
21
  on_message_callback: Callable[[dict, str], Coroutine[Any, Any, Any]],
34
- ):
35
- """Initialize the MQTT client."""
22
+ loop: asyncio.AbstractEventLoop,
23
+ ) -> None:
36
24
  self._auth = auth
25
+ """Initialize the LGHorizonMqttClient.
26
+
27
+ Args:
28
+ auth: The authentication object for obtaining MQTT tokens.
29
+ on_connected_callback: An async callback function for MQTT connection events.
30
+ on_message_callback: An async callback function for MQTT message events.
31
+ loop: The asyncio event loop.
32
+ """
37
33
  self._on_connected_callback = on_connected_callback
38
34
  self._on_message_callback = on_message_callback
39
- self._loop = asyncio.get_event_loop()
35
+ self._loop = loop
36
+
37
+ self._mqtt_client: mqtt.Client | None = None
38
+ self._mqtt_broker_url: str = ""
39
+ self._mqtt_token: str = ""
40
+ self.client_id: str = ""
41
+ self._reconnect_task: asyncio.Task | None = None
42
+ self._disconnect_requested: bool = False
43
+
44
+ # FIFO queues
45
+ self._message_queue: asyncio.Queue = asyncio.Queue()
46
+ self._publish_queue: asyncio.Queue = asyncio.Queue()
47
+
48
+ # Worker tasks
49
+ self._message_worker_task: asyncio.Task | None = None
50
+ self._publish_worker_task: asyncio.Task | None = None
51
+
52
+ @property
53
+ def is_connected(self) -> bool:
54
+ return self._mqtt_client is not None and self._mqtt_client.is_connected()
40
55
 
41
56
  @classmethod
42
57
  async def create(
@@ -44,80 +59,277 @@ class LGHorizonMqttClient:
44
59
  auth: LGHorizonAuth,
45
60
  on_connected_callback: Callable[[], Coroutine[Any, Any, Any]],
46
61
  on_message_callback: Callable[[dict, str], Coroutine[Any, Any, Any]],
47
- ):
48
- """Create the MQTT client."""
49
- instance = cls(auth, on_connected_callback, on_message_callback)
62
+ ) -> "LGHorizonMqttClient":
63
+ """Asynchronously create and initialize an LGHorizonMqttClient instance."""
64
+
65
+ loop = asyncio.get_running_loop()
66
+ instance = cls(auth, on_connected_callback, on_message_callback, loop)
67
+
68
+ # Service config ophalen
50
69
  service_config = await auth.get_service_config()
51
70
  mqtt_broker_url = await service_config.get_service_url("mqttBroker")
52
71
  instance._mqtt_broker_url = mqtt_broker_url.replace("wss://", "").replace(
53
72
  ":443/mqtt", ""
54
73
  )
74
+
55
75
  instance.client_id = await make_id()
76
+
77
+ # Paho client
56
78
  instance._mqtt_client = mqtt.Client(
57
79
  client_id=instance.client_id,
58
80
  transport="websockets",
59
81
  )
60
-
61
82
  instance._mqtt_client.ws_set_options(
62
83
  headers={"Sec-WebSocket-Protocol": "mqtt, mqttv3.1, mqttv3.11"}
63
84
  )
85
+
86
+ # Token ophalen
64
87
  instance._mqtt_token = await auth.get_mqtt_token()
65
- instance._mqtt_client.username_pw_set(auth.household_id, instance._mqtt_token)
66
- instance._mqtt_client.tls_set()
88
+ instance._mqtt_client.username_pw_set(
89
+ auth.household_id,
90
+ instance._mqtt_token,
91
+ )
92
+
93
+ # TLS instellen (blocking → executor)
94
+ await loop.run_in_executor(None, instance._mqtt_client.tls_set)
95
+
67
96
  instance._mqtt_client.enable_logger(_logger)
68
97
  instance._mqtt_client.on_connect = instance._on_connect
69
- instance._on_connected_callback = on_connected_callback
70
- instance._on_message_callback = on_message_callback
98
+ instance._mqtt_client.on_message = instance._on_message
99
+ instance._mqtt_client.on_disconnect = instance._on_disconnect
100
+
71
101
  return instance
72
102
 
73
- def _on_connect(self, client, userdata, flags, result_code): # pylint: disable=unused-argument
74
- if result_code == 0:
75
- self._mqtt_client.on_message = self._on_message
76
- if self._on_connected_callback:
77
- asyncio.run_coroutine_threadsafe(
78
- self._on_connected_callback(), self._loop
79
- )
80
- elif result_code == 5:
81
- self._mqtt_client.username_pw_set(self._auth.household_id, self._mqtt_token)
82
- asyncio.run_coroutine_threadsafe(self.connect(), self._loop)
83
- else:
84
- _logger.error(
85
- "Cannot connect to MQTT server with resultCode: %s", result_code
86
- )
103
+ async def connect(self) -> None:
104
+ """Connect the MQTT client to the broker asynchronously."""
105
+ if not self._mqtt_client:
106
+ raise RuntimeError("MQTT client not initialized")
87
107
 
88
- def _on_message(self, client, userdata, message): # pylint: disable=unused-argument
89
- """Wrapper for handling MQTT messages in a thread-safe manner."""
90
- asyncio.run_coroutine_threadsafe(
91
- self._on_client_message(client, userdata, message), self._loop
108
+ if self.is_connected:
109
+ _logger.debug("MQTT client is already connected.")
110
+ return
111
+
112
+ self._disconnect_requested = False # Reset flag for new connection attempt
113
+
114
+ # Cancel any ongoing reconnect task if connect() is called manually
115
+ if self._reconnect_task and not self._reconnect_task.done():
116
+ _logger.debug("Cancelling existing reconnect task before manual connect.")
117
+ self._reconnect_task.cancel()
118
+ self._reconnect_task = None
119
+
120
+ _logger.debug("Attempting initial MQTT connection...")
121
+ # Blocking connect → executor
122
+ await self._loop.run_in_executor(
123
+ None,
124
+ self._mqtt_client.connect,
125
+ self._mqtt_broker_url,
126
+ 443,
92
127
  )
93
128
 
94
- async def connect(self) -> None:
95
- """Connect the client."""
96
- self._mqtt_client.connect(self._mqtt_broker_url, 443)
129
+ # Start Paho thread
97
130
  self._mqtt_client.loop_start()
98
131
 
132
+ # Start workers
133
+ self._message_worker_task = asyncio.create_task(self._message_worker())
134
+ self._publish_worker_task = asyncio.create_task(self._publish_worker())
135
+
136
+ async def disconnect(self) -> None:
137
+ """Disconnect the MQTT client from the broker asynchronously."""
138
+ if not self._mqtt_client:
139
+ return
140
+
141
+ # Stop workers
142
+ if self._message_worker_task:
143
+ self._message_worker_task.cancel()
144
+ self._message_worker_task = None
145
+
146
+ if self._publish_worker_task:
147
+ self._publish_worker_task.cancel()
148
+ self._publish_worker_task = None
149
+
150
+ # Blocking disconnect → executor
151
+ await self._loop.run_in_executor(None, self._mqtt_client.disconnect)
152
+ self._mqtt_client.loop_stop()
153
+
99
154
  async def subscribe(self, topic: str) -> None:
100
- """Subscribe to a MQTT topic."""
155
+ """Subscribe to an MQTT topic.
156
+
157
+ Args:
158
+ topic: The MQTT topic to subscribe to.
159
+ """
160
+ if not self._mqtt_client:
161
+ raise RuntimeError("MQTT client not initialized")
162
+
101
163
  self._mqtt_client.subscribe(topic)
102
164
 
103
165
  async def publish_message(self, topic: str, json_payload: str) -> None:
104
- """Publish a MQTT message."""
105
- self._mqtt_client.publish(topic, json_payload, qos=2)
166
+ """Queue an MQTT message for publishing.
106
167
 
107
- async def disconnect(self) -> None:
108
- """Disconnect the client."""
109
- if self._mqtt_client.is_connected():
110
- self._mqtt_client.disconnect()
111
-
112
- async def _on_client_message(self, client, userdata, message): # pylint: disable=unused-argument
113
- """Handle messages received by mqtt client."""
114
- json_payload = await self._loop.run_in_executor(
115
- None, json.loads, message.payload
116
- )
117
- _logger.debug(
118
- "Received MQTT message \n\ntopic: %s\npayload:\n\n%s\n",
119
- message.topic,
120
- json.dumps(json_payload, indent=2),
168
+ Args:
169
+ topic: The MQTT topic to publish to.
170
+ json_payload: The JSON payload as a string.
171
+ """
172
+ await self._publish_queue.put((topic, json_payload))
173
+
174
+ # -------------------------
175
+ # INTERNAL CALLBACKS
176
+ # -------------------------
177
+
178
+ def _on_connect(self, client, userdata, flags, result_code):
179
+ """Callback for when the MQTT client connects to the broker.
180
+
181
+ Args:
182
+ client: The Paho MQTT client instance.
183
+ userdata: User data passed to the client.
184
+ flags: Response flags from the broker.
185
+ result_code: The connection result code.
186
+ """
187
+ if result_code == 0:
188
+ _logger.debug("MQTT client connected successfully.")
189
+ # If a reconnect task was running, it means we successfully reconnected.
190
+ # Cancel it as we are now connected.
191
+ if self._reconnect_task:
192
+ self._reconnect_task.cancel()
193
+ self._reconnect_task = None # Clear the reference
194
+ asyncio.run_coroutine_threadsafe(
195
+ self._on_connected_callback(),
196
+ self._loop,
197
+ )
198
+ elif result_code == 5:
199
+ _logger.debug(
200
+ "MQTT connection failed: Token expired. Attempting to refresh token and reconnect."
201
+ )
202
+ # Schedule the token refresh and reconnect in the main event loop
203
+ asyncio.run_coroutine_threadsafe(
204
+ self._handle_token_refresh_and_reconnect(), self._loop
205
+ )
206
+ else:
207
+ _logger.error("MQTT connect error: %s", result_code)
208
+ # For other errors, Paho's _on_disconnect will typically be called,
209
+ # which will then trigger the general reconnect loop.
210
+
211
+ async def _handle_token_refresh_and_reconnect(self):
212
+ """Refreshes the MQTT token and attempts to reconnect the client."""
213
+ try:
214
+ # Get new token
215
+ self._mqtt_token = await self._auth.get_mqtt_token()
216
+ self._mqtt_client.username_pw_set(
217
+ self._auth.household_id,
218
+ self._mqtt_token,
219
+ )
220
+ _logger.debug("MQTT token refreshed. Attempting to reconnect.")
221
+ # Call connect. If it fails, _on_disconnect will be triggered,
222
+ # and the _reconnect_loop will take over.
223
+ await self.connect()
224
+ except Exception as e:
225
+ _logger.error("Failed to refresh MQTT token or initiate reconnect: %s", e)
226
+ # If token refresh itself fails, or connect() raises an exception
227
+ # before _on_disconnect can be called, ensure reconnect loop starts.
228
+ if not self._disconnect_requested and (
229
+ not self._reconnect_task or self._reconnect_task.done()
230
+ ):
231
+ _logger.debug(
232
+ "Scheduling MQTT reconnect after token refresh/connect failure."
233
+ )
234
+ self._reconnect_task = asyncio.create_task(self._reconnect_loop())
235
+
236
+ def _on_message(self, client, userdata, message):
237
+ """Callback for when an MQTT message is received.
238
+
239
+ Args:
240
+ client: The Paho MQTT client instance.
241
+ userdata: User data passed to the client.
242
+ message: The MQTTMessage object containing topic and payload.
243
+ """
244
+ asyncio.run_coroutine_threadsafe(
245
+ self._message_queue.put((message.topic, message.payload)),
246
+ self._loop,
121
247
  )
122
- if self._on_message_callback:
123
- await self._on_message_callback(json_payload, message.topic)
248
+
249
+ def _on_disconnect(self, client, userdata, result_code):
250
+ """Callback for when the MQTT client disconnects from the broker.
251
+
252
+ Args:
253
+ client: The Paho MQTT client instance.
254
+ userdata: User data passed to the client.
255
+ result_code: The disconnection result code.
256
+ """
257
+ _logger.warning("MQTT disconnected with result code: %s", result_code)
258
+ if not self._disconnect_requested:
259
+ _logger.debug("Unexpected MQTT disconnection. Initiating reconnect loop.")
260
+ if not self._reconnect_task or self._reconnect_task.done():
261
+ self._reconnect_task = asyncio.run_coroutine_threadsafe(
262
+ self._reconnect_loop(), self._loop
263
+ )
264
+ else:
265
+ _logger.debug("Reconnect loop already active.")
266
+ else:
267
+ _logger.debug("MQTT disconnected as requested.")
268
+
269
+ async def _reconnect_loop(self):
270
+ """Manages the MQTT reconnection process with exponential backoff."""
271
+ retries = 0
272
+ while not self._disconnect_requested:
273
+ if self.is_connected:
274
+ _logger.debug(
275
+ "MQTT client reconnected within loop, stopping reconnect attempts."
276
+ )
277
+ break # Already connected, stop trying
278
+
279
+ delay = min(2**retries, 60) # Exponential backoff, max 60 seconds
280
+ _logger.debug(
281
+ "Waiting %s seconds before MQTT reconnect attempt %s",
282
+ delay,
283
+ retries + 1,
284
+ )
285
+ await asyncio.sleep(delay)
286
+
287
+ try:
288
+ _logger.debug("Attempting MQTT reconnect...")
289
+ await self.connect()
290
+ # If connect() succeeds, _on_connect will be called, which will cancel this task.
291
+ # If connect() fails, _on_disconnect will be called again, and this loop continues.
292
+ break # If connect() doesn't raise, assume it's handled by _on_connect
293
+ except Exception as e:
294
+ _logger.error("MQTT reconnect attempt failed: %s", e)
295
+ retries += 1
296
+ self._reconnect_task = None # Clear task when loop finishes or is cancelled.
297
+
298
+ # -------------------------
299
+ # MESSAGE WORKER (FIFO)
300
+ # -------------------------
301
+
302
+ async def _message_worker(self):
303
+ """Worker task to process incoming MQTT messages from the queue."""
304
+ while True:
305
+ topic, payload = await self._message_queue.get()
306
+
307
+ try:
308
+ json_payload = json.loads(payload)
309
+ await self._on_message_callback(json_payload, topic)
310
+ except Exception:
311
+ _logger.exception("Error processing MQTT message")
312
+
313
+ self._message_queue.task_done()
314
+
315
+ # -------------------------
316
+ # PUBLISH WORKER (FIFO)
317
+ # -------------------------
318
+
319
+ async def _publish_worker(self):
320
+ """Worker task to process outgoing MQTT publish commands from the queue."""
321
+ while True:
322
+ topic, payload = await self._publish_queue.get()
323
+
324
+ try:
325
+ # Wacht tot MQTT echt connected is
326
+ while not self.is_connected:
327
+ await asyncio.sleep(0.1)
328
+
329
+ # Publish is non-blocking
330
+ self._mqtt_client.publish(topic, payload, qos=2)
331
+
332
+ except Exception:
333
+ _logger.exception("Error publishing MQTT message")
334
+
335
+ self._publish_queue.task_done()
@@ -1,9 +1,11 @@
1
+ from typing import Optional
1
2
  from .lghorizon_models import (
2
3
  LGHorizonRecordingList,
3
4
  LGHorizonRecordingSingle,
4
5
  LGHorizonRecordingSeason,
5
6
  LGHorizonRecordingShow,
6
7
  LGHorizonRecordingType,
8
+ LGHorizonShowRecordingList,
7
9
  )
8
10
 
9
11
 
@@ -32,10 +34,22 @@ class LGHorizonRecordingFactory:
32
34
 
33
35
  return LGHorizonRecordingList(recording_list)
34
36
 
35
- async def create_episodes(self, episode_json: dict) -> LGHorizonRecordingList:
37
+ async def create_episodes(self, episode_json: dict) -> LGHorizonShowRecordingList:
36
38
  """Create a LGHorizonRecording list based for episodes."""
37
39
  recording_list = []
40
+ show_title: Optional[str] = None
41
+ if "images" in episode_json:
42
+ images = episode_json["images"]
43
+ show_image = next(
44
+ (img["url"] for img in images if img.get("type") == "titleTreatment"),
45
+ images[0]["url"] if images else None,
46
+ )
47
+ else:
48
+ show_image = None
49
+
38
50
  for recording in episode_json["data"]:
39
51
  recording_single = LGHorizonRecordingSingle(recording)
52
+ if show_title is None:
53
+ show_title = recording_single.show_title or recording_single.title
40
54
  recording_list.append(recording_single)
41
- return LGHorizonRecordingList(recording_list)
55
+ return LGHorizonShowRecordingList(show_title, show_image, recording_list)
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: lghorizon
3
+ Version: 0.9.1
4
+ Summary: Python client for Liberty Global Horizon settop boxes
5
+ Home-page: https://github.com/sholofly/LGHorizon-python
6
+ Author: Rudolf Offereins
7
+ Author-email: r.offereins@gmail.com
8
+ License: MIT license
9
+ Keywords: LG,Horizon,API,Settop box
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Natural Language :: English
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3.6
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: paho-mqtt
24
+ Requires-Dist: requests>=2.22.0
25
+ Requires-Dist: backoff>=1.9.0
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: keywords
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # LG Horizon API Python Library
40
+
41
+ A Python library to interact with and control LG Horizon set-top boxes. This library provides functionalities for authentication, real-time device status monitoring via MQTT, and various control commands for your Horizon devices.
42
+
43
+ ## Features
44
+
45
+ - **Authentication**: Supports authentication using username/password or a refresh token. The library automatically handles access token refreshing.
46
+ - **Device Management**: Discover and manage multiple LG Horizon set-top boxes associated with your account.
47
+ - **Real-time Status**: Monitor device status (online/running/standby) and current playback information (channel, show, VOD, recording, app) through MQTT.
48
+ - **Channel Information**: Retrieve a list of available channels and profile-specific favorite channels.
49
+ - **Recording Management**:
50
+ - Get a list of all recordings.
51
+ - Retrieve recordings for specific shows.
52
+ - Check recording quota and usage.
53
+ - **Device Control**: Send various commands to your set-top box:
54
+ - Power on/off.
55
+ - Play, pause, stop, rewind, fast forward.
56
+ - Change channels (up/down, direct channel selection).
57
+ - Record current program.
58
+ - Set player position for VOD/recordings.
59
+ - Display custom messages on the TV screen.
60
+ - Send emulated remote control key presses.
61
+ - **Robustness**: Includes automatic MQTT reconnection with exponential backoff and token refresh logic to maintain a stable connection.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install lghorizon-python # (Replace with actual package name if different)
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ Here's a basic example of how to use the library to connect to your LG Horizon devices and monitor their state:
72
+
73
+ First, create a `secrets.json` file in the root of your project with your LG Horizon credentials:
74
+
75
+ ```json
76
+ {
77
+ "username": "your_username",
78
+ "password": "your_password",
79
+ "country": "nl" // e.g., "nl" for Netherlands, "be" for Belgium
80
+ }
81
+ ```
82
+
83
+ Then, you can use the library as follows:
84
+
85
+ ```python
86
+ import asyncio
87
+ import json
88
+ import logging
89
+ import aiohttp
90
+
91
+ from lghorizon.lghorizon_api import LGHorizonApi
92
+ from lghorizon.lghorizon_models import LGHorizonAuth
93
+
94
+ _LOGGER = logging.getLogger(__name__)
95
+
96
+ async def main():
97
+ logging.basicConfig(level=logging.INFO) # Set to DEBUG for more verbose output
98
+
99
+ with open("secrets.json", encoding="utf-8") as f:
100
+ secrets = json.load(f)
101
+ username = secrets.get("username")
102
+ password = secrets.get("password")
103
+ country = secrets.get("country", "nl")
104
+
105
+ async with aiohttp.ClientSession() as session:
106
+ auth = LGHorizonAuth(session, country, username=username, password=password)
107
+ api = LGHorizonApi(auth)
108
+
109
+ async def device_state_changed_callback(device_id: str):
110
+ device = devices[device_id]
111
+ _LOGGER.info(
112
+ f"Device {device.device_friendly_name} ({device.device_id}) state changed:\n"
113
+ f" State: {device.device_state.state.value}\n"
114
+ f" UI State: {device.device_state.ui_state_type.value}\n"
115
+ f" Source Type: {device.device_state.source_type.value}\n"
116
+ f" Channel: {device.device_state.channel_name or 'N/A'} ({device.device_state.channel_id or 'N/A'})\n"
117
+ f" Show: {device.device_state.show_title or 'N/A'}\n"
118
+ f" Episode: {device.device_state.episode_title or 'N/A'}\n"
119
+ f" Position: {device.device_state.position or 'N/A'} / {device.device_state.duration or 'N/A'}\n"
120
+ )
121
+
122
+ try:
123
+ _LOGGER.info("Initializing LG Horizon API...")
124
+ await api.initialize()
125
+ devices = await api.get_devices()
126
+
127
+ for device in devices.values():
128
+ _LOGGER.info(f"Registering callback for device: {device.device_friendly_name}")
129
+ await device.set_callback(device_state_changed_callback)
130
+
131
+ _LOGGER.info("API initialized. Monitoring device states. Press Ctrl+C to exit.")
132
+ # Keep the script running to receive MQTT updates
133
+ while True:
134
+ await asyncio.sleep(3600) # Sleep for a long time, MQTT callbacks will still fire
135
+
136
+ except Exception as e:
137
+ _LOGGER.error(f"An error occurred: {e}", exc_info=True)
138
+ finally:
139
+ _LOGGER.info("Disconnecting from LG Horizon API.")
140
+ await api.disconnect()
141
+ _LOGGER.info("Disconnected.")
142
+
143
+ if __name__ == "__main__":
144
+ asyncio.run(main())
145
+ ```
146
+
147
+ ## Authentication
148
+
149
+ The `LGHorizonAuth` class handles authentication. You can initialize it with a username and password, or directly with a refresh token if you have one. The library automatically refreshes access tokens as needed.
150
+
151
+ ```python
152
+ # Using username and password
153
+ auth = LGHorizonAuth(session, "nl", username="your_username", password="your_password")
154
+
155
+ # Using a refresh token (e.g., if you've saved it from a previous session)
156
+ # auth = LGHorizonAuth(session, "nl", refresh_token="your_refresh_token")
157
+ ```
158
+
159
+ You can also set a callback to receive the updated refresh token when it's refreshed, allowing you to persist it for future sessions:
160
+
161
+ ```python
162
+ def token_updated_callback(new_refresh_token: str):
163
+ print(f"New refresh token received: {new_refresh_token}")
164
+ # Here you would typically save this new_refresh_token
165
+ # to your secrets.json or other persistent storage.
166
+
167
+ # After initializing LGHorizonApi:
168
+ # api.set_token_refresh_callback(token_updated_callback)
169
+ ```
170
+
171
+ ## Error Handling
172
+
173
+ The library defines custom exceptions for common error scenarios:
174
+
175
+ - `LGHorizonApiError`: Base exception for all API-related errors.
176
+ - `LGHorizonApiConnectionError`: Raised for network or connection issues.
177
+ - `LGHorizonApiUnauthorizedError`: Raised when authentication fails (e.g., invalid credentials).
178
+ - `LGHorizonApiLockedError`: A specific type of `LGHorizonApiUnauthorizedError` indicating a locked account.
179
+
180
+ These exceptions allow for more granular error handling in your application.
181
+
182
+ ## Development
183
+
184
+ To run the example script (`main.py`) from the repository:
185
+
186
+ 1. Clone this repository.
187
+ 2. Install dependencies: `pip install -r requirements.txt` (ensure `requirements.txt` is up-to-date).
188
+ 3. Create a `secrets.json` file as described in the Usage section.
189
+ 4. Run `python main.py`.
@@ -0,0 +1,17 @@
1
+ lghorizon/__init__.py,sha256=wowOg2VYhvb1Nzbpx02aNyc4tiUjCfZgV680smPASXE,1913
2
+ lghorizon/const.py,sha256=TrEptIbYSf4eoW9llb7vvyDdUFJ0ObvX0M0qay6kaHY,2694
3
+ lghorizon/exceptions.py,sha256=4cnnPRuKBtqXiTlOZK4xmP1WADikk5NvCsy293mijT8,500
4
+ lghorizon/helpers.py,sha256=SGlEN6V0kh2vqw1qCKmM1KhfeO-UvPyyQmnThgFLFhs,272
5
+ lghorizon/lghorizon_api.py,sha256=_yP7X3LIei3OPc_byPPZg22QrPkDQdRyMoR6GLg3c3M,12881
6
+ lghorizon/lghorizon_device.py,sha256=HfpbOnmiN44Y0ObSj-lCP8vYQ85WBYU8rbBaqvDzri8,14767
7
+ lghorizon/lghorizon_device_state_processor.py,sha256=LW_Gavhg7bSSeZJiprAi1YJqOcUhZcB-2GdHn9hMj-o,15380
8
+ lghorizon/lghorizon_message_factory.py,sha256=BkJrVgcTsidcugrM9Pt5brvdw5_WI4PRAHvWQyHurz8,1435
9
+ lghorizon/lghorizon_models.py,sha256=-xhTIz1UC5efR5HJtXf2a59EspDbZKGp967flYvhryM,46435
10
+ lghorizon/lghorizon_mqtt_client.py,sha256=e1gyaF0awyVyQZRdJHyZmYcY9omlWnoUwVrvQ5CdMtY,12769
11
+ lghorizon/lghorizon_recording_factory.py,sha256=JaFjmXWChp1jdFzkioe9K-MqzVU0AsOgdKGjm2Seyq0,2318
12
+ lghorizon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ lghorizon-0.9.1.dist-info/licenses/LICENSE,sha256=6Dh2tur1gMX3r3rITjVwUONBEJxyyPZDY8p6DZXtimE,1059
14
+ lghorizon-0.9.1.dist-info/METADATA,sha256=OOtLMZiWEW9gqy5kaRVlnbTzfdObTgASQ2HIz5m0u60,7453
15
+ lghorizon-0.9.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
+ lghorizon-0.9.1.dist-info/top_level.txt,sha256=usii76_AxGfPI6gjrrh-NyZxcQQuF1B8_Q9kd7sID8Q,10
17
+ lghorizon-0.9.1.dist-info/RECORD,,