lghorizon 0.9.0__py3-none-any.whl → 0.9.0.dev1__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.
- lghorizon/__init__.py +23 -58
- lghorizon/{lghorizon_device_state_processor.py → device_state_processor.py} +34 -98
- lghorizon/{lghorizon_api.py → lghorizonapi.py} +27 -57
- lghorizon/{lghorizon_message_factory.py → message_factory.py} +2 -1
- lghorizon/{lghorizon_recording_factory.py → recording_factory.py} +3 -17
- lghorizon-0.9.0.dev1.dist-info/METADATA +41 -0
- lghorizon-0.9.0.dev1.dist-info/RECORD +13 -0
- lghorizon/exceptions.py +0 -17
- lghorizon/lghorizon_device.py +0 -409
- lghorizon/lghorizon_models.py +0 -1512
- lghorizon/lghorizon_mqtt_client.py +0 -335
- lghorizon-0.9.0.dist-info/METADATA +0 -191
- lghorizon-0.9.0.dist-info/RECORD +0 -17
- {lghorizon-0.9.0.dist-info → lghorizon-0.9.0.dev1.dist-info}/WHEEL +0 -0
- {lghorizon-0.9.0.dist-info → lghorizon-0.9.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {lghorizon-0.9.0.dist-info → lghorizon-0.9.0.dev1.dist-info}/top_level.txt +0 -0
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
from typing import Any, Callable, Coroutine
|
|
5
|
-
|
|
6
|
-
import paho.mqtt.client as mqtt
|
|
7
|
-
|
|
8
|
-
from .helpers import make_id
|
|
9
|
-
from .lghorizon_models import LGHorizonAuth
|
|
10
|
-
|
|
11
|
-
_logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class LGHorizonMqttClient:
|
|
15
|
-
"""Asynchronous-friendly wrapper around Paho MQTT."""
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
auth: LGHorizonAuth,
|
|
20
|
-
on_connected_callback: Callable[[], Coroutine[Any, Any, Any]],
|
|
21
|
-
on_message_callback: Callable[[dict, str], Coroutine[Any, Any, Any]],
|
|
22
|
-
loop: asyncio.AbstractEventLoop,
|
|
23
|
-
) -> None:
|
|
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
|
-
"""
|
|
33
|
-
self._on_connected_callback = on_connected_callback
|
|
34
|
-
self._on_message_callback = on_message_callback
|
|
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()
|
|
55
|
-
|
|
56
|
-
@classmethod
|
|
57
|
-
async def create(
|
|
58
|
-
cls,
|
|
59
|
-
auth: LGHorizonAuth,
|
|
60
|
-
on_connected_callback: Callable[[], Coroutine[Any, Any, Any]],
|
|
61
|
-
on_message_callback: Callable[[dict, str], Coroutine[Any, Any, Any]],
|
|
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
|
|
69
|
-
service_config = await auth.get_service_config()
|
|
70
|
-
mqtt_broker_url = await service_config.get_service_url("mqttBroker")
|
|
71
|
-
instance._mqtt_broker_url = mqtt_broker_url.replace("wss://", "").replace(
|
|
72
|
-
":443/mqtt", ""
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
instance.client_id = await make_id()
|
|
76
|
-
|
|
77
|
-
# Paho client
|
|
78
|
-
instance._mqtt_client = mqtt.Client(
|
|
79
|
-
client_id=instance.client_id,
|
|
80
|
-
transport="websockets",
|
|
81
|
-
)
|
|
82
|
-
instance._mqtt_client.ws_set_options(
|
|
83
|
-
headers={"Sec-WebSocket-Protocol": "mqtt, mqttv3.1, mqttv3.11"}
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
# Token ophalen
|
|
87
|
-
instance._mqtt_token = await auth.get_mqtt_token()
|
|
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
|
-
|
|
96
|
-
instance._mqtt_client.enable_logger(_logger)
|
|
97
|
-
instance._mqtt_client.on_connect = instance._on_connect
|
|
98
|
-
instance._mqtt_client.on_message = instance._on_message
|
|
99
|
-
instance._mqtt_client.on_disconnect = instance._on_disconnect
|
|
100
|
-
|
|
101
|
-
return instance
|
|
102
|
-
|
|
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")
|
|
107
|
-
|
|
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.info("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,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Start Paho thread
|
|
130
|
-
self._mqtt_client.loop_start()
|
|
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
|
-
|
|
154
|
-
async def subscribe(self, topic: str) -> None:
|
|
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
|
-
|
|
163
|
-
self._mqtt_client.subscribe(topic)
|
|
164
|
-
|
|
165
|
-
async def publish_message(self, topic: str, json_payload: str) -> None:
|
|
166
|
-
"""Queue an MQTT message for publishing.
|
|
167
|
-
|
|
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.info("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.warning(
|
|
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.info("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.info(
|
|
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,
|
|
247
|
-
)
|
|
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.info("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.info("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.info("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,191 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lghorizon
|
|
3
|
-
Version: 0.9.0
|
|
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
|
|
40
|
-
|
|
41
|
-
# LG Horizon API Python Library
|
|
42
|
-
|
|
43
|
-
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.
|
|
44
|
-
|
|
45
|
-
## Features
|
|
46
|
-
|
|
47
|
-
- **Authentication**: Supports authentication using username/password or a refresh token. The library automatically handles access token refreshing.
|
|
48
|
-
- **Device Management**: Discover and manage multiple LG Horizon set-top boxes associated with your account.
|
|
49
|
-
- **Real-time Status**: Monitor device status (online/running/standby) and current playback information (channel, show, VOD, recording, app) through MQTT.
|
|
50
|
-
- **Channel Information**: Retrieve a list of available channels and profile-specific favorite channels.
|
|
51
|
-
- **Recording Management**:
|
|
52
|
-
- Get a list of all recordings.
|
|
53
|
-
- Retrieve recordings for specific shows.
|
|
54
|
-
- Check recording quota and usage.
|
|
55
|
-
- **Device Control**: Send various commands to your set-top box:
|
|
56
|
-
- Power on/off.
|
|
57
|
-
- Play, pause, stop, rewind, fast forward.
|
|
58
|
-
- Change channels (up/down, direct channel selection).
|
|
59
|
-
- Record current program.
|
|
60
|
-
- Set player position for VOD/recordings.
|
|
61
|
-
- Display custom messages on the TV screen.
|
|
62
|
-
- Send emulated remote control key presses.
|
|
63
|
-
- **Robustness**: Includes automatic MQTT reconnection with exponential backoff and token refresh logic to maintain a stable connection.
|
|
64
|
-
|
|
65
|
-
## Installation
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
pip install lghorizon-python # (Replace with actual package name if different)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Usage
|
|
72
|
-
|
|
73
|
-
Here's a basic example of how to use the library to connect to your LG Horizon devices and monitor their state:
|
|
74
|
-
|
|
75
|
-
First, create a `secrets.json` file in the root of your project with your LG Horizon credentials:
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"username": "your_username",
|
|
80
|
-
"password": "your_password",
|
|
81
|
-
"country": "nl" // e.g., "nl" for Netherlands, "be" for Belgium
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Then, you can use the library as follows:
|
|
86
|
-
|
|
87
|
-
```python
|
|
88
|
-
import asyncio
|
|
89
|
-
import json
|
|
90
|
-
import logging
|
|
91
|
-
import aiohttp
|
|
92
|
-
|
|
93
|
-
from lghorizon.lghorizon_api import LGHorizonApi
|
|
94
|
-
from lghorizon.lghorizon_models import LGHorizonAuth
|
|
95
|
-
|
|
96
|
-
_LOGGER = logging.getLogger(__name__)
|
|
97
|
-
|
|
98
|
-
async def main():
|
|
99
|
-
logging.basicConfig(level=logging.INFO) # Set to DEBUG for more verbose output
|
|
100
|
-
|
|
101
|
-
with open("secrets.json", encoding="utf-8") as f:
|
|
102
|
-
secrets = json.load(f)
|
|
103
|
-
username = secrets.get("username")
|
|
104
|
-
password = secrets.get("password")
|
|
105
|
-
country = secrets.get("country", "nl")
|
|
106
|
-
|
|
107
|
-
async with aiohttp.ClientSession() as session:
|
|
108
|
-
auth = LGHorizonAuth(session, country, username=username, password=password)
|
|
109
|
-
api = LGHorizonApi(auth)
|
|
110
|
-
|
|
111
|
-
async def device_state_changed_callback(device_id: str):
|
|
112
|
-
device = devices[device_id]
|
|
113
|
-
_LOGGER.info(
|
|
114
|
-
f"Device {device.device_friendly_name} ({device.device_id}) state changed:\n"
|
|
115
|
-
f" State: {device.device_state.state.value}\n"
|
|
116
|
-
f" UI State: {device.device_state.ui_state_type.value}\n"
|
|
117
|
-
f" Source Type: {device.device_state.source_type.value}\n"
|
|
118
|
-
f" Channel: {device.device_state.channel_name or 'N/A'} ({device.device_state.channel_id or 'N/A'})\n"
|
|
119
|
-
f" Show: {device.device_state.show_title or 'N/A'}\n"
|
|
120
|
-
f" Episode: {device.device_state.episode_title or 'N/A'}\n"
|
|
121
|
-
f" Position: {device.device_state.position or 'N/A'} / {device.device_state.duration or 'N/A'}\n"
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
_LOGGER.info("Initializing LG Horizon API...")
|
|
126
|
-
await api.initialize()
|
|
127
|
-
devices = await api.get_devices()
|
|
128
|
-
|
|
129
|
-
for device in devices.values():
|
|
130
|
-
_LOGGER.info(f"Registering callback for device: {device.device_friendly_name}")
|
|
131
|
-
await device.set_callback(device_state_changed_callback)
|
|
132
|
-
|
|
133
|
-
_LOGGER.info("API initialized. Monitoring device states. Press Ctrl+C to exit.")
|
|
134
|
-
# Keep the script running to receive MQTT updates
|
|
135
|
-
while True:
|
|
136
|
-
await asyncio.sleep(3600) # Sleep for a long time, MQTT callbacks will still fire
|
|
137
|
-
|
|
138
|
-
except Exception as e:
|
|
139
|
-
_LOGGER.error(f"An error occurred: {e}", exc_info=True)
|
|
140
|
-
finally:
|
|
141
|
-
_LOGGER.info("Disconnecting from LG Horizon API.")
|
|
142
|
-
await api.disconnect()
|
|
143
|
-
_LOGGER.info("Disconnected.")
|
|
144
|
-
|
|
145
|
-
if __name__ == "__main__":
|
|
146
|
-
asyncio.run(main())
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Authentication
|
|
150
|
-
|
|
151
|
-
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.
|
|
152
|
-
|
|
153
|
-
```python
|
|
154
|
-
# Using username and password
|
|
155
|
-
auth = LGHorizonAuth(session, "nl", username="your_username", password="your_password")
|
|
156
|
-
|
|
157
|
-
# Using a refresh token (e.g., if you've saved it from a previous session)
|
|
158
|
-
# auth = LGHorizonAuth(session, "nl", refresh_token="your_refresh_token")
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
You can also set a callback to receive the updated refresh token when it's refreshed, allowing you to persist it for future sessions:
|
|
162
|
-
|
|
163
|
-
```python
|
|
164
|
-
def token_updated_callback(new_refresh_token: str):
|
|
165
|
-
print(f"New refresh token received: {new_refresh_token}")
|
|
166
|
-
# Here you would typically save this new_refresh_token
|
|
167
|
-
# to your secrets.json or other persistent storage.
|
|
168
|
-
|
|
169
|
-
# After initializing LGHorizonApi:
|
|
170
|
-
# api.set_token_refresh_callback(token_updated_callback)
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Error Handling
|
|
174
|
-
|
|
175
|
-
The library defines custom exceptions for common error scenarios:
|
|
176
|
-
|
|
177
|
-
- `LGHorizonApiError`: Base exception for all API-related errors.
|
|
178
|
-
- `LGHorizonApiConnectionError`: Raised for network or connection issues.
|
|
179
|
-
- `LGHorizonApiUnauthorizedError`: Raised when authentication fails (e.g., invalid credentials).
|
|
180
|
-
- `LGHorizonApiLockedError`: A specific type of `LGHorizonApiUnauthorizedError` indicating a locked account.
|
|
181
|
-
|
|
182
|
-
These exceptions allow for more granular error handling in your application.
|
|
183
|
-
|
|
184
|
-
## Development
|
|
185
|
-
|
|
186
|
-
To run the example script (`main.py`) from the repository:
|
|
187
|
-
|
|
188
|
-
1. Clone this repository.
|
|
189
|
-
2. Install dependencies: `pip install -r requirements.txt` (ensure `requirements.txt` is up-to-date).
|
|
190
|
-
3. Create a `secrets.json` file as described in the Usage section.
|
|
191
|
-
4. Run `python main.py`.
|
lghorizon-0.9.0.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
lghorizon/__init__.py,sha256=y44i_P6jsl3VV0R4UYIymXpRXIJVYPwb97uQK9pEfYA,1852
|
|
2
|
-
lghorizon/const.py,sha256=HINlbyevEN9ZRnfIBbSGNc6i9J8WkIgpqkLZrwyqpGQ,5307
|
|
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=6BsZkRo3PBf62F2530_FdQ4OMr9w1jiewcv19Y8MWWo,12764
|
|
11
|
-
lghorizon/lghorizon_recording_factory.py,sha256=JaFjmXWChp1jdFzkioe9K-MqzVU0AsOgdKGjm2Seyq0,2318
|
|
12
|
-
lghorizon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
lghorizon-0.9.0.dist-info/licenses/LICENSE,sha256=6Dh2tur1gMX3r3rITjVwUONBEJxyyPZDY8p6DZXtimE,1059
|
|
14
|
-
lghorizon-0.9.0.dist-info/METADATA,sha256=HJvdaN47p25UvHdotYhrI5BIPUeTmqJbFqfad7rJRwY,7471
|
|
15
|
-
lghorizon-0.9.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
16
|
-
lghorizon-0.9.0.dist-info/top_level.txt,sha256=usii76_AxGfPI6gjrrh-NyZxcQQuF1B8_Q9kd7sID8Q,10
|
|
17
|
-
lghorizon-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|