lghorizon 0.7.5b2__py3-none-any.whl → 0.8.0b2__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 +19 -1
- lghorizon/helpers.py +3 -2
- lghorizon/lghorizon_api.py +111 -102
- lghorizon/models.py +185 -136
- {lghorizon-0.7.5b2.dist-info → lghorizon-0.8.0b2.dist-info}/METADATA +2 -2
- lghorizon-0.8.0b2.dist-info/RECORD +12 -0
- lghorizon-0.7.5b2.dist-info/RECORD +0 -12
- {lghorizon-0.7.5b2.dist-info → lghorizon-0.8.0b2.dist-info}/LICENSE +0 -0
- {lghorizon-0.7.5b2.dist-info → lghorizon-0.8.0b2.dist-info}/WHEEL +0 -0
- {lghorizon-0.7.5b2.dist-info → lghorizon-0.8.0b2.dist-info}/top_level.txt +0 -0
lghorizon/__init__.py
CHANGED
|
@@ -20,4 +20,22 @@ from .const import (
|
|
|
20
20
|
RECORDING_TYPE_SHOW,
|
|
21
21
|
RECORDING_TYPE_SEASON,
|
|
22
22
|
RECORDING_TYPE_SINGLE,
|
|
23
|
-
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"LGHorizonApi",
|
|
27
|
+
"LGHorizonBox",
|
|
28
|
+
"LGHorizonRecordingListSeasonShow",
|
|
29
|
+
"LGHorizonRecordingSingle",
|
|
30
|
+
"LGHorizonRecordingShow",
|
|
31
|
+
"LGHorizonRecordingEpisode",
|
|
32
|
+
"LGHorizonCustomer",
|
|
33
|
+
"LGHorizonApiUnauthorizedError",
|
|
34
|
+
"LGHorizonApiConnectionError",
|
|
35
|
+
"LGHorizonApiLockedError",
|
|
36
|
+
"ONLINE_RUNNING",
|
|
37
|
+
"ONLINE_STANDBY",
|
|
38
|
+
"RECORDING_TYPE_SHOW",
|
|
39
|
+
"RECORDING_TYPE_SEASON",
|
|
40
|
+
"RECORDING_TYPE_SINGLE",
|
|
41
|
+
] # noqa
|
lghorizon/helpers.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Helper functions."""
|
|
2
|
+
|
|
2
3
|
import random
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def make_id(
|
|
6
|
+
def make_id(string_length=10):
|
|
6
7
|
"""Create an id with given length."""
|
|
7
8
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
8
|
-
return "".join(random.choice(letters) for i in range(
|
|
9
|
+
return "".join(random.choice(letters) for i in range(string_length))
|
lghorizon/lghorizon_api.py
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"""Python client for LGHorizon."""
|
|
2
|
+
# pylint: disable=broad-exception-caught
|
|
3
|
+
# pylint: disable=line-too-long
|
|
2
4
|
|
|
3
5
|
import logging
|
|
4
6
|
import json
|
|
5
|
-
import
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, List
|
|
10
|
+
import backoff
|
|
11
|
+
|
|
12
|
+
from requests import Session, exceptions as request_exceptions
|
|
13
|
+
|
|
6
14
|
from .exceptions import (
|
|
7
15
|
LGHorizonApiUnauthorizedError,
|
|
8
16
|
LGHorizonApiConnectionError,
|
|
9
17
|
LGHorizonApiLockedError,
|
|
10
18
|
)
|
|
11
|
-
|
|
12
|
-
from requests import Session, exceptions as request_exceptions
|
|
13
|
-
from paho.mqtt.client import WebsocketConnectionError
|
|
14
|
-
import re
|
|
19
|
+
|
|
15
20
|
from .models import (
|
|
16
21
|
LGHorizonAuth,
|
|
17
22
|
LGHorizonBox,
|
|
@@ -39,7 +44,7 @@ from .const import (
|
|
|
39
44
|
RECORDING_TYPE_SEASON,
|
|
40
45
|
RECORDING_TYPE_SHOW,
|
|
41
46
|
)
|
|
42
|
-
|
|
47
|
+
|
|
43
48
|
|
|
44
49
|
_logger = logging.getLogger(__name__)
|
|
45
50
|
_supported_platforms = ["EOS", "EOS2", "HORIZON", "APOLLO"]
|
|
@@ -52,7 +57,7 @@ class LGHorizonApi:
|
|
|
52
57
|
_session: Session = None
|
|
53
58
|
settop_boxes: Dict[str, LGHorizonBox] = None
|
|
54
59
|
customer: LGHorizonCustomer = None
|
|
55
|
-
|
|
60
|
+
_mqtt_client: LGHorizonMqttClient = None
|
|
56
61
|
_channels: Dict[str, LGHorizonChannel] = None
|
|
57
62
|
_country_settings = None
|
|
58
63
|
_country_code: str = None
|
|
@@ -89,11 +94,9 @@ class LGHorizonApi:
|
|
|
89
94
|
def _authorize(self) -> None:
|
|
90
95
|
ctry_code = self._country_code[0:2]
|
|
91
96
|
if ctry_code == "be":
|
|
92
|
-
self.
|
|
97
|
+
self._authorize_telenet()
|
|
93
98
|
elif ctry_code in ("gb", "ch"):
|
|
94
|
-
self.
|
|
95
|
-
# elif ctry_code == "ch":
|
|
96
|
-
# self.authorize_sunrise()
|
|
99
|
+
self._authorize_with_refresh_token()
|
|
97
100
|
else:
|
|
98
101
|
self._authorize_default()
|
|
99
102
|
|
|
@@ -124,7 +127,8 @@ class LGHorizonApi:
|
|
|
124
127
|
self._auth.fill(auth_response.json())
|
|
125
128
|
_logger.debug("Authorization succeeded")
|
|
126
129
|
|
|
127
|
-
def
|
|
130
|
+
def _authorize_with_refresh_token(self) -> None:
|
|
131
|
+
"""Handle authorizzationg using request token."""
|
|
128
132
|
_logger.debug("Authorizing via refresh")
|
|
129
133
|
refresh_url = (
|
|
130
134
|
f"{self._country_settings['api_url']}/auth-service/v1/authorization/refresh"
|
|
@@ -153,8 +157,8 @@ class LGHorizonApi:
|
|
|
153
157
|
raise LGHorizonApiConnectionError("Unknown connection error")
|
|
154
158
|
|
|
155
159
|
self._auth.fill(auth_response.json())
|
|
156
|
-
self.refresh_token = self._auth.
|
|
157
|
-
self._session.cookies["ACCESSTOKEN"] = self._auth.
|
|
160
|
+
self.refresh_token = self._auth.refresh_token
|
|
161
|
+
self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
|
|
158
162
|
|
|
159
163
|
if self._refresh_callback:
|
|
160
164
|
self._refresh_callback()
|
|
@@ -162,9 +166,11 @@ class LGHorizonApi:
|
|
|
162
166
|
_logger.debug("Authorization succeeded")
|
|
163
167
|
|
|
164
168
|
def set_callback(self, refresh_callback: Callable) -> None:
|
|
169
|
+
"""Set the refresh callback."""
|
|
165
170
|
self._refresh_callback = refresh_callback
|
|
166
171
|
|
|
167
|
-
def
|
|
172
|
+
def _authorize_telenet(self):
|
|
173
|
+
"""Authorize telenet users."""
|
|
168
174
|
try:
|
|
169
175
|
login_session = Session()
|
|
170
176
|
# Step 1 - Get Authorization data
|
|
@@ -176,13 +182,13 @@ class LGHorizonApi:
|
|
|
176
182
|
if not auth_response.ok:
|
|
177
183
|
raise LGHorizonApiConnectionError("Can't connect to authorization URL")
|
|
178
184
|
auth_response_json = auth_response.json()
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
authorization_uri = auth_response_json["authorizationUri"]
|
|
186
|
+
authorization_validity_token = auth_response_json["validityToken"]
|
|
181
187
|
|
|
182
188
|
# Step 2 - Get Authorization cookie
|
|
183
189
|
_logger.debug("Step 2 - Get Authorization cookie")
|
|
184
190
|
|
|
185
|
-
auth_cookie_response = login_session.get(
|
|
191
|
+
auth_cookie_response = login_session.get(authorization_uri)
|
|
186
192
|
if not auth_cookie_response.ok:
|
|
187
193
|
raise LGHorizonApiConnectionError("Can't connect to authorization URL")
|
|
188
194
|
|
|
@@ -206,20 +212,20 @@ class LGHorizonApi:
|
|
|
206
212
|
self._country_settings["oauth_redirect_header"]
|
|
207
213
|
]
|
|
208
214
|
|
|
209
|
-
if
|
|
215
|
+
if self._identifier is not None:
|
|
210
216
|
redirect_url += f"&dtv_identifier={self._identifier}"
|
|
211
217
|
redirect_response = login_session.get(redirect_url, allow_redirects=False)
|
|
212
218
|
success_url = redirect_response.headers[
|
|
213
219
|
self._country_settings["oauth_redirect_header"]
|
|
214
220
|
]
|
|
215
|
-
|
|
221
|
+
code_matches = re.findall(r"code=(.*)&", success_url)
|
|
216
222
|
|
|
217
|
-
|
|
223
|
+
authorization_code = code_matches[0]
|
|
218
224
|
|
|
219
225
|
new_payload = {
|
|
220
226
|
"authorizationGrant": {
|
|
221
|
-
"authorizationCode":
|
|
222
|
-
"validityToken":
|
|
227
|
+
"authorizationCode": authorization_code,
|
|
228
|
+
"validityToken": authorization_validity_token,
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
headers = {
|
|
@@ -229,8 +235,8 @@ class LGHorizonApi:
|
|
|
229
235
|
auth_url, json.dumps(new_payload), headers=headers
|
|
230
236
|
)
|
|
231
237
|
self._auth.fill(post_result.json())
|
|
232
|
-
self._session.cookies["ACCESSTOKEN"] = self._auth.
|
|
233
|
-
except Exception
|
|
238
|
+
self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
|
|
239
|
+
except Exception:
|
|
234
240
|
pass
|
|
235
241
|
|
|
236
242
|
def _obtain_mqtt_token(self):
|
|
@@ -238,7 +244,7 @@ class LGHorizonApi:
|
|
|
238
244
|
mqtt_auth_url = self._config["authorizationService"]["URL"]
|
|
239
245
|
mqtt_response = self._do_api_call(f"{mqtt_auth_url}/v1/mqtt/token")
|
|
240
246
|
self._auth.mqttToken = mqtt_response["token"]
|
|
241
|
-
_logger.debug(
|
|
247
|
+
_logger.debug("MQTT token: %s", self._auth.mqttToken)
|
|
242
248
|
|
|
243
249
|
@backoff.on_exception(
|
|
244
250
|
backoff.expo,
|
|
@@ -251,11 +257,12 @@ class LGHorizonApi:
|
|
|
251
257
|
),
|
|
252
258
|
)
|
|
253
259
|
def connect(self) -> None:
|
|
260
|
+
"""Start connection process."""
|
|
254
261
|
self._config = self._get_config(self._country_code)
|
|
255
262
|
_logger.debug("Connect to API")
|
|
256
263
|
self._authorize()
|
|
257
264
|
self._obtain_mqtt_token()
|
|
258
|
-
self.
|
|
265
|
+
self._mqtt_client = LGHorizonMqttClient(
|
|
259
266
|
self._auth,
|
|
260
267
|
self._config["mqttBroker"]["URL"],
|
|
261
268
|
self._on_mqtt_connected,
|
|
@@ -263,14 +270,14 @@ class LGHorizonApi:
|
|
|
263
270
|
)
|
|
264
271
|
|
|
265
272
|
self._register_customer_and_boxes()
|
|
266
|
-
self.
|
|
273
|
+
self._mqtt_client.connect()
|
|
267
274
|
|
|
268
275
|
def disconnect(self):
|
|
269
276
|
"""Disconnect."""
|
|
270
277
|
_logger.debug("Disconnect from API")
|
|
271
|
-
if not self.
|
|
278
|
+
if not self._mqtt_client or not self._mqtt_client.is_connected:
|
|
272
279
|
return
|
|
273
|
-
self.
|
|
280
|
+
self._mqtt_client.disconnect()
|
|
274
281
|
|
|
275
282
|
def _on_mqtt_connected(self) -> None:
|
|
276
283
|
_logger.debug("Connected to MQTT server. Registering all boxes...")
|
|
@@ -281,47 +288,46 @@ class LGHorizonApi:
|
|
|
281
288
|
def _on_mqtt_message(self, message: str, topic: str) -> None:
|
|
282
289
|
if "action" in message and message["action"] == "OPS.getProfilesUpdate":
|
|
283
290
|
self._update_customer()
|
|
284
|
-
box: LGHorizonBox
|
|
285
291
|
elif "source" in message:
|
|
286
|
-
|
|
287
|
-
if not isinstance(
|
|
292
|
+
device_id = message["source"]
|
|
293
|
+
if not isinstance(device_id, str):
|
|
288
294
|
_logger.debug("ignoring message - not a string")
|
|
289
295
|
return
|
|
290
|
-
if not
|
|
296
|
+
if device_id not in self.settop_boxes:
|
|
291
297
|
return
|
|
292
298
|
try:
|
|
293
299
|
if "deviceType" in message and message["deviceType"] == "STB":
|
|
294
|
-
self.settop_boxes[
|
|
300
|
+
self.settop_boxes[device_id].update_state(message)
|
|
295
301
|
if "status" in message:
|
|
296
|
-
self._handle_box_update(
|
|
302
|
+
self._handle_box_update(device_id, message)
|
|
297
303
|
|
|
298
304
|
except Exception:
|
|
299
305
|
_logger.exception("Could not handle status message")
|
|
300
|
-
_logger.warning(
|
|
301
|
-
self.settop_boxes[
|
|
302
|
-
self.settop_boxes[
|
|
306
|
+
_logger.warning("Full message: %s", str(message))
|
|
307
|
+
self.settop_boxes[device_id].playing_info.reset()
|
|
308
|
+
self.settop_boxes[device_id].playing_info.set_paused(False)
|
|
303
309
|
elif "CPE.capacity" in message:
|
|
304
310
|
splitted_topic = topic.split("/")
|
|
305
311
|
if len(splitted_topic) != 4:
|
|
306
312
|
return
|
|
307
|
-
|
|
308
|
-
if not
|
|
313
|
+
device_id = splitted_topic[1]
|
|
314
|
+
if device_id not in self.settop_boxes:
|
|
309
315
|
return
|
|
310
|
-
self.settop_boxes[
|
|
316
|
+
self.settop_boxes[device_id].update_recording_capacity(message)
|
|
311
317
|
|
|
312
|
-
def _handle_box_update(self,
|
|
313
|
-
|
|
314
|
-
if "uiStatus" not in
|
|
318
|
+
def _handle_box_update(self, device_id: str, raw_message: Any) -> None:
|
|
319
|
+
status_payload = raw_message["status"]
|
|
320
|
+
if "uiStatus" not in status_payload:
|
|
315
321
|
return
|
|
316
|
-
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
if "sourceType" not in
|
|
322
|
+
ui_status = status_payload["uiStatus"]
|
|
323
|
+
if ui_status == "mainUI":
|
|
324
|
+
player_state = status_payload["playerState"]
|
|
325
|
+
if "sourceType" not in player_state or "source" not in player_state:
|
|
320
326
|
return
|
|
321
|
-
source_type =
|
|
322
|
-
state_source =
|
|
323
|
-
self.settop_boxes[
|
|
324
|
-
|
|
327
|
+
source_type = player_state["sourceType"]
|
|
328
|
+
state_source = player_state["source"]
|
|
329
|
+
self.settop_boxes[device_id].playing_info.set_paused(
|
|
330
|
+
player_state["speed"] == 0
|
|
325
331
|
)
|
|
326
332
|
if (
|
|
327
333
|
source_type
|
|
@@ -332,27 +338,27 @@ class LGHorizonApi:
|
|
|
332
338
|
)
|
|
333
339
|
and "eventId" in state_source
|
|
334
340
|
):
|
|
335
|
-
|
|
341
|
+
event_id = state_source["eventId"]
|
|
336
342
|
raw_replay_event = self._do_api_call(
|
|
337
|
-
f"{self._config['linearService']['URL']}/v2/replayEvent/{
|
|
343
|
+
f"{self._config['linearService']['URL']}/v2/replayEvent/{event_id}?returnLinearContent=true&language={self._country_settings['language']}"
|
|
338
344
|
)
|
|
339
|
-
|
|
340
|
-
channel = self._channels[
|
|
341
|
-
self.settop_boxes[
|
|
342
|
-
source_type,
|
|
345
|
+
replay_event = LGHorizonReplayEvent(raw_replay_event)
|
|
346
|
+
channel = self._channels[replay_event.channel_id]
|
|
347
|
+
self.settop_boxes[device_id].update_with_replay_event(
|
|
348
|
+
source_type, replay_event, channel
|
|
343
349
|
)
|
|
344
350
|
elif source_type == BOX_PLAY_STATE_DVR:
|
|
345
|
-
|
|
351
|
+
recording_id = state_source["recordingId"]
|
|
346
352
|
session_start_time = state_source["sessionStartTime"]
|
|
347
353
|
session_end_time = state_source["sessionEndTime"]
|
|
348
|
-
last_speed_change_time =
|
|
349
|
-
relative_position =
|
|
354
|
+
last_speed_change_time = player_state["lastSpeedChangeTime"]
|
|
355
|
+
relative_position = player_state["relativePosition"]
|
|
350
356
|
raw_recording = self._do_api_call(
|
|
351
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.
|
|
357
|
+
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/details/single/{recording_id}?profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&language={self._country_settings['language']}"
|
|
352
358
|
)
|
|
353
359
|
recording = LGHorizonRecordingSingle(raw_recording)
|
|
354
|
-
channel = self._channels[recording.
|
|
355
|
-
self.settop_boxes[
|
|
360
|
+
channel = self._channels[recording.channel_id]
|
|
361
|
+
self.settop_boxes[device_id].update_with_recording(
|
|
356
362
|
source_type,
|
|
357
363
|
recording,
|
|
358
364
|
channel,
|
|
@@ -362,35 +368,35 @@ class LGHorizonApi:
|
|
|
362
368
|
relative_position,
|
|
363
369
|
)
|
|
364
370
|
elif source_type == BOX_PLAY_STATE_VOD:
|
|
365
|
-
|
|
366
|
-
last_speed_change_time =
|
|
367
|
-
relative_position =
|
|
371
|
+
title_id = state_source["titleId"]
|
|
372
|
+
last_speed_change_time = player_state["lastSpeedChangeTime"]
|
|
373
|
+
relative_position = player_state["relativePosition"]
|
|
368
374
|
raw_vod = self._do_api_call(
|
|
369
|
-
f"{self._config['vodService']['URL']}/v2/detailscreen/{
|
|
375
|
+
f"{self._config['vodService']['URL']}/v2/detailscreen/{title_id}?language={self._country_settings['language']}&profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&cityId={self.customer.city_id}"
|
|
370
376
|
)
|
|
371
377
|
vod = LGHorizonVod(raw_vod)
|
|
372
|
-
self.settop_boxes[
|
|
378
|
+
self.settop_boxes[device_id].update_with_vod(
|
|
373
379
|
source_type, vod, last_speed_change_time, relative_position
|
|
374
380
|
)
|
|
375
|
-
elif
|
|
376
|
-
app = LGHorizonApp(
|
|
377
|
-
self.settop_boxes[
|
|
381
|
+
elif ui_status == "apps":
|
|
382
|
+
app = LGHorizonApp(status_payload["appsState"])
|
|
383
|
+
self.settop_boxes[device_id].update_with_app("app", app)
|
|
378
384
|
|
|
379
385
|
@backoff.on_exception(
|
|
380
386
|
backoff.expo, LGHorizonApiConnectionError, max_tries=3, logger=_logger
|
|
381
387
|
)
|
|
382
|
-
def _do_api_call(self, url: str
|
|
383
|
-
_logger.info(
|
|
388
|
+
def _do_api_call(self, url: str) -> str:
|
|
389
|
+
_logger.info("Executing API call to %s", url)
|
|
384
390
|
try:
|
|
385
391
|
api_response = self._session.get(url)
|
|
386
392
|
api_response.raise_for_status()
|
|
387
393
|
json_response = api_response.json()
|
|
388
|
-
except request_exceptions.HTTPError as
|
|
394
|
+
except request_exceptions.HTTPError as http_ex:
|
|
389
395
|
self._authorize()
|
|
390
396
|
raise LGHorizonApiConnectionError(
|
|
391
|
-
f"Unable to call {url}. Error:{str(
|
|
392
|
-
)
|
|
393
|
-
_logger.debug(
|
|
397
|
+
f"Unable to call {url}. Error:{str(http_ex)}"
|
|
398
|
+
) from http_ex
|
|
399
|
+
_logger.debug("Result API call: %s", json_response)
|
|
394
400
|
return json_response
|
|
395
401
|
|
|
396
402
|
def _register_customer_and_boxes(self):
|
|
@@ -412,15 +418,15 @@ class LGHorizonApi:
|
|
|
412
418
|
else:
|
|
413
419
|
platform_type = None
|
|
414
420
|
box = LGHorizonBox(
|
|
415
|
-
device, platform_type, self.
|
|
421
|
+
device, platform_type, self._mqtt_client, self._auth, self._channels
|
|
416
422
|
)
|
|
417
|
-
self.settop_boxes[box.
|
|
418
|
-
_logger.info("Box %s registered...", box.
|
|
423
|
+
self.settop_boxes[box.device_id] = box
|
|
424
|
+
_logger.info("Box %s registered...", box.device_id)
|
|
419
425
|
|
|
420
426
|
def _update_customer(self):
|
|
421
427
|
_logger.info("Get customer data")
|
|
422
428
|
personalisation_result = self._do_api_call(
|
|
423
|
-
f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.
|
|
429
|
+
f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.household_id}?with=profiles%2Cdevices"
|
|
424
430
|
)
|
|
425
431
|
_logger.debug("Personalisation result: %s ", personalisation_result)
|
|
426
432
|
self.customer = LGHorizonCustomer(personalisation_result)
|
|
@@ -429,7 +435,7 @@ class LGHorizonApi:
|
|
|
429
435
|
self._update_entitlements()
|
|
430
436
|
_logger.info("Retrieving channels...")
|
|
431
437
|
channels_result = self._do_api_call(
|
|
432
|
-
f"{self._config['linearService']['URL']}/v2/channels?cityId={self.customer.
|
|
438
|
+
f"{self._config['linearService']['URL']}/v2/channels?cityId={self.customer.city_id}&language={self._country_settings['language']}&productClass=Orion-DASH"
|
|
433
439
|
)
|
|
434
440
|
for channel in channels_result:
|
|
435
441
|
if "isRadio" in channel and channel["isRadio"]:
|
|
@@ -441,9 +447,10 @@ class LGHorizonApi:
|
|
|
441
447
|
continue
|
|
442
448
|
channel_id = channel["id"]
|
|
443
449
|
self._channels[channel_id] = LGHorizonChannel(channel)
|
|
444
|
-
_logger.info(
|
|
450
|
+
_logger.info("%s retrieved.", len(self._channels))
|
|
445
451
|
|
|
446
452
|
def get_display_channels(self):
|
|
453
|
+
"""Returns channels to display baed on profile."""
|
|
447
454
|
all_channels = self._channels.values()
|
|
448
455
|
if not self._profile_id or self._profile_id not in self.customer.profiles:
|
|
449
456
|
return all_channels
|
|
@@ -455,11 +462,11 @@ class LGHorizonApi:
|
|
|
455
462
|
channel for channel in all_channels if channel.id in profile_channel_ids
|
|
456
463
|
]
|
|
457
464
|
|
|
458
|
-
def _get_replay_event(self,
|
|
465
|
+
def _get_replay_event(self, listing_id) -> Any:
|
|
459
466
|
"""Get listing."""
|
|
460
467
|
_logger.info("Retrieving replay event details...")
|
|
461
468
|
response = self._do_api_call(
|
|
462
|
-
f"{self._config['linearService']['URL']}/v2/replayEvent/{
|
|
469
|
+
f"{self._config['linearService']['URL']}/v2/replayEvent/{listing_id}?returnLinearContent=true&language={self._country_settings['language']}"
|
|
463
470
|
)
|
|
464
471
|
_logger.info("Replay event details retrieved")
|
|
465
472
|
return response
|
|
@@ -473,38 +480,40 @@ class LGHorizonApi:
|
|
|
473
480
|
try:
|
|
474
481
|
_logger.info("Retrieving recordingcapacity...")
|
|
475
482
|
quota_content = self._do_api_call(
|
|
476
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.
|
|
483
|
+
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/quota"
|
|
477
484
|
)
|
|
478
|
-
if
|
|
485
|
+
if "quota" not in quota_content and "occupied" not in quota_content:
|
|
479
486
|
_logger.error("Unable to fetch recording capacity...")
|
|
480
487
|
return None
|
|
481
488
|
capacity = (quota_content["occupied"] / quota_content["quota"]) * 100
|
|
482
489
|
self.recording_capacity = round(capacity)
|
|
483
|
-
_logger.debug(
|
|
490
|
+
_logger.debug("Remaining recordingcapacity %s %%", self.recording_capacity)
|
|
484
491
|
return self.recording_capacity
|
|
485
|
-
except:
|
|
492
|
+
except Exception:
|
|
486
493
|
_logger.error("Unable to fetch recording capacity...")
|
|
487
494
|
return None
|
|
488
495
|
|
|
489
496
|
def get_recordings(self) -> List[LGHorizonBaseRecording]:
|
|
497
|
+
"""Returns recordings."""
|
|
490
498
|
_logger.info("Retrieving recordings...")
|
|
491
499
|
recording_content = self._do_api_call(
|
|
492
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.
|
|
500
|
+
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/recordings?sort=time&sortOrder=desc&language={self._country_settings['language']}"
|
|
493
501
|
)
|
|
494
502
|
recordings = []
|
|
495
503
|
for recording_data_item in recording_content["data"]:
|
|
496
|
-
|
|
497
|
-
if
|
|
504
|
+
recording_type = recording_data_item["type"]
|
|
505
|
+
if recording_type == RECORDING_TYPE_SINGLE:
|
|
498
506
|
recordings.append(LGHorizonRecordingSingle(recording_data_item))
|
|
499
|
-
elif
|
|
507
|
+
elif recording_type in (RECORDING_TYPE_SEASON, RECORDING_TYPE_SHOW):
|
|
500
508
|
recordings.append(LGHorizonRecordingListSeasonShow(recording_data_item))
|
|
501
|
-
_logger.info(
|
|
509
|
+
_logger.info("%s recordings retrieved...", len(recordings))
|
|
502
510
|
return recordings
|
|
503
511
|
|
|
504
|
-
def get_recording_show(self,
|
|
512
|
+
def get_recording_show(self, show_id: str) -> list[LGHorizonRecordingSingle]:
|
|
513
|
+
"""Returns show recording"""
|
|
505
514
|
_logger.info("Retrieving show recordings...")
|
|
506
515
|
show_recording_content = self._do_api_call(
|
|
507
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.
|
|
516
|
+
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/episodes/shows/{show_id}?source=recording&language=nl&sort=time&sortOrder=asc"
|
|
508
517
|
)
|
|
509
518
|
recordings = []
|
|
510
519
|
for item in show_recording_content["data"]:
|
|
@@ -512,21 +521,21 @@ class LGHorizonApi:
|
|
|
512
521
|
recordings.append(LGHorizonRecordingShow(item))
|
|
513
522
|
else:
|
|
514
523
|
recordings.append(LGHorizonRecordingEpisode(item))
|
|
515
|
-
_logger.info(
|
|
524
|
+
_logger.info("%s showrecordings retrieved...", len(recordings))
|
|
516
525
|
return recordings
|
|
517
526
|
|
|
518
527
|
def _update_entitlements(self) -> None:
|
|
519
528
|
_logger.info("Retrieving entitlements...")
|
|
520
529
|
entitlements_json = self._do_api_call(
|
|
521
|
-
f"{self._config['purchaseService']['URL']}/v2/customers/{self._auth.
|
|
530
|
+
f"{self._config['purchaseService']['URL']}/v2/customers/{self._auth.household_id}/entitlements?enableDaypass=true"
|
|
522
531
|
)
|
|
523
532
|
self._entitlements.clear()
|
|
524
533
|
for entitlement in entitlements_json["entitlements"]:
|
|
525
534
|
self._entitlements.append(entitlement["id"])
|
|
526
535
|
|
|
527
536
|
def _get_config(self, country_code: str):
|
|
528
|
-
|
|
529
|
-
config_url = f"{self._country_settings['api_url']}/{
|
|
537
|
+
base_country_code = country_code[0:2]
|
|
538
|
+
config_url = f"{self._country_settings['api_url']}/{base_country_code}/en/config-service/conf/web/backoffice.json"
|
|
530
539
|
result = self._do_api_call(config_url)
|
|
531
540
|
_logger.debug(result)
|
|
532
541
|
return result
|
lghorizon/models.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
"""Models for LGHorizon API."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=broad-exception-caught
|
|
4
|
+
# pylint: disable=broad-exception-raised
|
|
1
5
|
from datetime import datetime
|
|
2
|
-
from typing import
|
|
6
|
+
from typing import Callable, Dict
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
3
9
|
import paho.mqtt.client as mqtt
|
|
4
|
-
|
|
10
|
+
|
|
5
11
|
from .const import (
|
|
6
|
-
BOX_PLAY_STATE_BUFFER,
|
|
7
12
|
BOX_PLAY_STATE_CHANNEL,
|
|
8
|
-
BOX_PLAY_STATE_DVR,
|
|
9
|
-
BOX_PLAY_STATE_REPLAY,
|
|
10
|
-
BOX_PLAY_STATE_APP,
|
|
11
|
-
BOX_PLAY_STATE_VOD,
|
|
12
13
|
ONLINE_STANDBY,
|
|
13
14
|
ONLINE_RUNNING,
|
|
14
15
|
MEDIA_KEY_POWER,
|
|
@@ -21,46 +22,47 @@ from .const import (
|
|
|
21
22
|
MEDIA_KEY_FAST_FORWARD,
|
|
22
23
|
MEDIA_KEY_RECORD,
|
|
23
24
|
RECORDING_TYPE_SEASON,
|
|
24
|
-
RECORDING_TYPE_SHOW,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
import json
|
|
28
27
|
from .helpers import make_id
|
|
29
|
-
import logging
|
|
30
28
|
|
|
31
29
|
_logger = logging.getLogger(__name__)
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
class LGHorizonAuth:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
"""Class to hold LGHorizon authentication."""
|
|
34
|
+
|
|
35
|
+
household_id: str
|
|
36
|
+
access_token: str
|
|
37
|
+
refresh_token: str
|
|
38
|
+
refresh_token_expiry: datetime
|
|
39
39
|
username: str
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
mqtt_token: str = None
|
|
41
|
+
access_token: str = None
|
|
42
42
|
|
|
43
43
|
def __init__(self):
|
|
44
44
|
"""Initialize a session."""
|
|
45
|
-
pass
|
|
46
45
|
|
|
47
46
|
def fill(self, auth_json) -> None:
|
|
48
|
-
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
47
|
+
"""Fill the object."""
|
|
48
|
+
self.household_id = auth_json["householdId"]
|
|
49
|
+
self.access_token = auth_json["accessToken"]
|
|
50
|
+
self.refresh_token = auth_json["refreshToken"]
|
|
51
51
|
self.username = auth_json["username"]
|
|
52
52
|
try:
|
|
53
|
-
self.
|
|
53
|
+
self.refresh_token_expiry = datetime.fromtimestamp(
|
|
54
54
|
auth_json["refreshTokenExpiry"]
|
|
55
55
|
)
|
|
56
56
|
except ValueError:
|
|
57
|
-
# VM uses milliseconds for the expiry time
|
|
58
|
-
|
|
57
|
+
# VM uses milliseconds for the expiry time.
|
|
58
|
+
# If the year is too high to be valid, it assumes it's milliseconds and divides it
|
|
59
|
+
self.refresh_token_expiry = datetime.fromtimestamp(
|
|
59
60
|
auth_json["refreshTokenExpiry"] // 1000
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
def is_expired(self) -> bool:
|
|
63
|
-
|
|
64
|
+
"""Check if refresh token is expired."""
|
|
65
|
+
return self.refresh_token_expiry
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
class LGHorizonPlayingInfo:
|
|
@@ -78,7 +80,6 @@ class LGHorizonPlayingInfo:
|
|
|
78
80
|
|
|
79
81
|
def __init__(self):
|
|
80
82
|
"""Initialize the playing info."""
|
|
81
|
-
pass
|
|
82
83
|
|
|
83
84
|
def set_paused(self, paused: bool):
|
|
84
85
|
"""Set pause state."""
|
|
@@ -117,11 +118,13 @@ class LGHorizonPlayingInfo:
|
|
|
117
118
|
self.last_position_update = last_position_update
|
|
118
119
|
|
|
119
120
|
def reset_progress(self):
|
|
121
|
+
"""Reset the progress."""
|
|
120
122
|
self.last_position_update = None
|
|
121
123
|
self.duration = None
|
|
122
124
|
self.position = None
|
|
123
125
|
|
|
124
126
|
def reset(self):
|
|
127
|
+
"""Reset the channel"""
|
|
125
128
|
self.channel_id = None
|
|
126
129
|
self.title = None
|
|
127
130
|
self.image = None
|
|
@@ -152,6 +155,7 @@ class LGHorizonChannel:
|
|
|
152
155
|
self.channel_number = channel_json["logicalChannelNumber"]
|
|
153
156
|
|
|
154
157
|
def get_stream_image(self, channel_json) -> str:
|
|
158
|
+
"""Returns the stream image."""
|
|
155
159
|
image_stream = channel_json["imageStream"]
|
|
156
160
|
if "full" in image_stream:
|
|
157
161
|
return image_stream["full"]
|
|
@@ -163,47 +167,56 @@ class LGHorizonChannel:
|
|
|
163
167
|
|
|
164
168
|
|
|
165
169
|
class LGHorizonReplayEvent:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
"""LGhorizon replay event."""
|
|
171
|
+
|
|
172
|
+
episode_number: int = None
|
|
173
|
+
channel_id: str = None
|
|
174
|
+
event_id: str = None
|
|
175
|
+
season_number: int = None
|
|
170
176
|
title: str = None
|
|
171
|
-
|
|
177
|
+
episode_name: str = None
|
|
172
178
|
|
|
173
179
|
def __init__(self, raw_json: str):
|
|
174
|
-
self.
|
|
175
|
-
self.
|
|
180
|
+
self.channel_id = raw_json["channelId"]
|
|
181
|
+
self.event_id = raw_json["eventId"]
|
|
176
182
|
self.title = raw_json["title"]
|
|
177
183
|
if "episodeName" in raw_json:
|
|
178
|
-
self.
|
|
184
|
+
self.episode_name = raw_json["episodeName"]
|
|
179
185
|
if "episodeNumber" in raw_json:
|
|
180
|
-
self.
|
|
186
|
+
self.episode_number = raw_json["episodeNumber"]
|
|
181
187
|
if "seasonNumber" in raw_json:
|
|
182
|
-
self.
|
|
188
|
+
self.season_number = raw_json["seasonNumber"]
|
|
183
189
|
|
|
184
190
|
|
|
185
191
|
class LGHorizonBaseRecording:
|
|
186
|
-
|
|
192
|
+
"""LgHorizon base recording."""
|
|
193
|
+
|
|
194
|
+
recording_id: str = None
|
|
187
195
|
title: str = None
|
|
188
196
|
image: str = None
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
recording_type: str = None
|
|
198
|
+
channel_id: str = None
|
|
191
199
|
|
|
192
200
|
def __init__(
|
|
193
|
-
self,
|
|
201
|
+
self,
|
|
202
|
+
recording_id: str,
|
|
203
|
+
title: str,
|
|
204
|
+
image: str,
|
|
205
|
+
channel_id: str,
|
|
206
|
+
recording_type: str,
|
|
194
207
|
) -> None:
|
|
195
|
-
self.
|
|
208
|
+
self.recording_id = recording_id
|
|
196
209
|
self.title = title
|
|
197
210
|
self.image = image
|
|
198
|
-
self.
|
|
199
|
-
self.
|
|
211
|
+
self.channel_id = channel_id
|
|
212
|
+
self.recording_type = recording_type
|
|
200
213
|
|
|
201
214
|
|
|
202
215
|
class LGHorizonRecordingSingle(LGHorizonBaseRecording):
|
|
203
216
|
"""Represents a single recording."""
|
|
204
217
|
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
season_number: int = None
|
|
219
|
+
episode_number: int = None
|
|
207
220
|
|
|
208
221
|
def __init__(self, recording_json):
|
|
209
222
|
"""Init the single recording."""
|
|
@@ -219,32 +232,32 @@ class LGHorizonRecordingSingle(LGHorizonBaseRecording):
|
|
|
219
232
|
recording_json["type"],
|
|
220
233
|
)
|
|
221
234
|
if "seasonNumber" in recording_json:
|
|
222
|
-
self.
|
|
235
|
+
self.season_number = recording_json["seasonNumber"]
|
|
223
236
|
if "episodeNumber" in recording_json:
|
|
224
|
-
self.
|
|
237
|
+
self.episode_number = recording_json["episodeNumber"]
|
|
225
238
|
|
|
226
239
|
|
|
227
240
|
class LGHorizonRecordingEpisode:
|
|
228
241
|
"""Represents a single recording."""
|
|
229
242
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
243
|
+
episode_id: str = None
|
|
244
|
+
episode_title: str = None
|
|
245
|
+
season_number: int = None
|
|
246
|
+
episode_number: int = None
|
|
247
|
+
show_title: str = None
|
|
248
|
+
recording_state: str = None
|
|
236
249
|
image: str = None
|
|
237
250
|
|
|
238
251
|
def __init__(self, recording_json):
|
|
239
252
|
"""Init the single recording."""
|
|
240
|
-
self.
|
|
241
|
-
self.
|
|
242
|
-
self.
|
|
243
|
-
self.
|
|
253
|
+
self.episode_id = recording_json["episodeId"]
|
|
254
|
+
self.episode_title = recording_json["episodeTitle"]
|
|
255
|
+
self.show_title = recording_json["showTitle"]
|
|
256
|
+
self.recording_state = recording_json["recordingState"]
|
|
244
257
|
if "seasonNumber" in recording_json:
|
|
245
|
-
self.
|
|
258
|
+
self.season_number = recording_json["seasonNumber"]
|
|
246
259
|
if "episodeNumber" in recording_json:
|
|
247
|
-
self.
|
|
260
|
+
self.episode_number = recording_json["episodeNumber"]
|
|
248
261
|
if "poster" in recording_json and "url" in recording_json["poster"]:
|
|
249
262
|
self.image = recording_json["poster"]["url"]
|
|
250
263
|
|
|
@@ -252,28 +265,30 @@ class LGHorizonRecordingEpisode:
|
|
|
252
265
|
class LGHorizonRecordingShow:
|
|
253
266
|
"""Represents a single recording."""
|
|
254
267
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
episode_id: str = None
|
|
269
|
+
show_title: str = None
|
|
270
|
+
season_number: int = None
|
|
271
|
+
episode_number: int = None
|
|
272
|
+
recording_state: str = None
|
|
260
273
|
image: str = None
|
|
261
274
|
|
|
262
275
|
def __init__(self, recording_json):
|
|
263
276
|
"""Init the single recording."""
|
|
264
|
-
self.
|
|
265
|
-
self.
|
|
266
|
-
self.
|
|
277
|
+
self.episode_id = recording_json["episodeId"]
|
|
278
|
+
self.show_title = recording_json["showTitle"]
|
|
279
|
+
self.recording_state = recording_json["recordingState"]
|
|
267
280
|
if "seasonNumber" in recording_json:
|
|
268
|
-
self.
|
|
281
|
+
self.season_number = recording_json["seasonNumber"]
|
|
269
282
|
if "episodeNumber" in recording_json:
|
|
270
|
-
self.
|
|
283
|
+
self.episode_number = recording_json["episodeNumber"]
|
|
271
284
|
if "poster" in recording_json and "url" in recording_json["poster"]:
|
|
272
285
|
self.image = recording_json["poster"]["url"]
|
|
273
286
|
|
|
274
287
|
|
|
275
288
|
class LGHorizonRecordingListSeasonShow(LGHorizonBaseRecording):
|
|
276
|
-
|
|
289
|
+
"""LGHorizon Season show list."""
|
|
290
|
+
|
|
291
|
+
show_id: str = None
|
|
277
292
|
|
|
278
293
|
def __init__(self, recording_season_json):
|
|
279
294
|
"""Init the single recording."""
|
|
@@ -286,13 +301,15 @@ class LGHorizonRecordingListSeasonShow(LGHorizonBaseRecording):
|
|
|
286
301
|
recording_season_json["channelId"],
|
|
287
302
|
recording_season_json["type"],
|
|
288
303
|
)
|
|
289
|
-
if self.
|
|
290
|
-
self.
|
|
304
|
+
if self.recording_type == RECORDING_TYPE_SEASON:
|
|
305
|
+
self.show_id = recording_season_json["showId"]
|
|
291
306
|
else:
|
|
292
|
-
self.
|
|
307
|
+
self.show_id = recording_season_json["id"]
|
|
293
308
|
|
|
294
309
|
|
|
295
310
|
class LGHorizonVod:
|
|
311
|
+
"""LGHorizon video on demand."""
|
|
312
|
+
|
|
296
313
|
title: str = None
|
|
297
314
|
image: str = None
|
|
298
315
|
duration: float = None
|
|
@@ -303,6 +320,8 @@ class LGHorizonVod:
|
|
|
303
320
|
|
|
304
321
|
|
|
305
322
|
class LGHorizonApp:
|
|
323
|
+
"""LGHorizon App."""
|
|
324
|
+
|
|
306
325
|
title: str = None
|
|
307
326
|
image: str = None
|
|
308
327
|
|
|
@@ -314,15 +333,18 @@ class LGHorizonApp:
|
|
|
314
333
|
|
|
315
334
|
|
|
316
335
|
class LGHorizonMqttClient:
|
|
317
|
-
|
|
336
|
+
"""LGHorizon MQTT client."""
|
|
337
|
+
|
|
338
|
+
_broker_url: str = None
|
|
318
339
|
_mqtt_client: mqtt.Client
|
|
319
340
|
_auth: LGHorizonAuth
|
|
320
|
-
|
|
341
|
+
client_id: str = None
|
|
321
342
|
_on_connected_callback: Callable = None
|
|
322
343
|
_on_message_callback: Callable[[str, str], None] = None
|
|
323
344
|
|
|
324
345
|
@property
|
|
325
346
|
def is_connected(self):
|
|
347
|
+
"""Is client connected."""
|
|
326
348
|
return self._mqtt_client.is_connected
|
|
327
349
|
|
|
328
350
|
def __init__(
|
|
@@ -333,80 +355,95 @@ class LGHorizonMqttClient:
|
|
|
333
355
|
on_message_callback: Callable[[str], None] = None,
|
|
334
356
|
):
|
|
335
357
|
self._auth = auth
|
|
336
|
-
self.
|
|
337
|
-
|
|
338
|
-
|
|
358
|
+
self._broker_url = mqtt_broker_url.replace("wss://", "").replace(
|
|
359
|
+
":443/mqtt", ""
|
|
360
|
+
)
|
|
361
|
+
self.client_id = make_id()
|
|
362
|
+
self._mqtt_client = mqtt.Client(
|
|
363
|
+
mqtt.CallbackAPIVersion.VERSION1,
|
|
364
|
+
client_id=self.client_id,
|
|
365
|
+
transport="websockets",
|
|
366
|
+
)
|
|
339
367
|
|
|
340
368
|
self._mqtt_client.ws_set_options(
|
|
341
369
|
headers={"Sec-WebSocket-Protocol": "mqtt, mqttv3.1, mqttv3.11"}
|
|
342
370
|
)
|
|
343
|
-
self._mqtt_client.username_pw_set(
|
|
371
|
+
self._mqtt_client.username_pw_set(
|
|
372
|
+
self._auth.household_id, self._auth.mqtt_token
|
|
373
|
+
)
|
|
344
374
|
self._mqtt_client.tls_set()
|
|
345
375
|
self._mqtt_client.enable_logger(_logger)
|
|
346
376
|
self._mqtt_client.on_connect = self._on_mqtt_connect
|
|
347
377
|
self._on_connected_callback = on_connected_callback
|
|
348
378
|
self._on_message_callback = on_message_callback
|
|
349
379
|
|
|
350
|
-
def _on_mqtt_connect(self, client, userdata, flags,
|
|
351
|
-
if
|
|
380
|
+
def _on_mqtt_connect(self, client, userdata, flags, result_code): # pylint: disable=unused-argument
|
|
381
|
+
if result_code == 0:
|
|
352
382
|
self._mqtt_client.on_message = self._on_client_message
|
|
353
|
-
self._mqtt_client.subscribe(self._auth.
|
|
354
|
-
self._mqtt_client.subscribe(self._auth.
|
|
355
|
-
self._mqtt_client.subscribe(self._auth.
|
|
356
|
-
self._mqtt_client.subscribe(self._auth.
|
|
357
|
-
self._mqtt_client.subscribe(
|
|
383
|
+
self._mqtt_client.subscribe(self._auth.household_id)
|
|
384
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/#")
|
|
385
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/" + self.client_id)
|
|
386
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/+/status")
|
|
387
|
+
self._mqtt_client.subscribe(
|
|
388
|
+
self._auth.household_id + "/+/networkRecordings"
|
|
389
|
+
)
|
|
358
390
|
self._mqtt_client.subscribe(
|
|
359
|
-
self._auth.
|
|
391
|
+
self._auth.household_id + "/+/networkRecordings/capacity"
|
|
360
392
|
)
|
|
361
|
-
self._mqtt_client.subscribe(self._auth.
|
|
393
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/+/localRecordings")
|
|
362
394
|
self._mqtt_client.subscribe(
|
|
363
|
-
self._auth.
|
|
395
|
+
self._auth.household_id + "/+/localRecordings/capacity"
|
|
364
396
|
)
|
|
365
|
-
self._mqtt_client.subscribe(self._auth.
|
|
366
|
-
self._mqtt_client.subscribe(self._auth.
|
|
397
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/watchlistService")
|
|
398
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/purchaseService")
|
|
367
399
|
self._mqtt_client.subscribe(
|
|
368
|
-
self._auth.
|
|
400
|
+
self._auth.household_id + "/personalizationService"
|
|
369
401
|
)
|
|
370
|
-
self._mqtt_client.subscribe(self._auth.
|
|
402
|
+
self._mqtt_client.subscribe(self._auth.household_id + "/recordingStatus")
|
|
371
403
|
self._mqtt_client.subscribe(
|
|
372
|
-
self._auth.
|
|
404
|
+
self._auth.household_id + "/recordingStatus/lastUserAction"
|
|
373
405
|
)
|
|
374
406
|
if self._on_connected_callback:
|
|
375
407
|
self._on_connected_callback()
|
|
376
|
-
elif
|
|
408
|
+
elif result_code == 5:
|
|
377
409
|
self._mqtt_client.username_pw_set(
|
|
378
|
-
self._auth.
|
|
410
|
+
self._auth.household_id, self._auth.mqtt_token
|
|
379
411
|
)
|
|
380
412
|
self.connect()
|
|
381
413
|
else:
|
|
382
414
|
_logger.error(
|
|
383
|
-
|
|
415
|
+
"Cannot connect to MQTT server with resultCode: %s", result_code
|
|
384
416
|
)
|
|
385
417
|
|
|
386
418
|
def connect(self) -> None:
|
|
387
|
-
|
|
419
|
+
"""Connect the client."""
|
|
420
|
+
self._mqtt_client.connect(self._broker_url, 443)
|
|
388
421
|
self._mqtt_client.loop_start()
|
|
389
422
|
|
|
390
|
-
def _on_client_message(self, client, userdata, message):
|
|
423
|
+
def _on_client_message(self, client, userdata, message): # pylint: disable=unused-argument
|
|
391
424
|
"""Handle messages received by mqtt client."""
|
|
392
|
-
_logger.debug(
|
|
393
|
-
|
|
394
|
-
_logger.debug(
|
|
425
|
+
_logger.debug("Received MQTT message. Topic: %s", message.topic)
|
|
426
|
+
json_payload = json.loads(message.payload)
|
|
427
|
+
_logger.debug("Message: %s", json_payload)
|
|
395
428
|
if self._on_message_callback:
|
|
396
|
-
self._on_message_callback(
|
|
429
|
+
self._on_message_callback(json_payload, message.topic)
|
|
397
430
|
|
|
398
431
|
def publish_message(self, topic: str, json_payload: str) -> None:
|
|
432
|
+
"""Publish a MQTT message."""
|
|
399
433
|
self._mqtt_client.publish(topic, json_payload, qos=2)
|
|
400
434
|
|
|
401
435
|
def disconnect(self) -> None:
|
|
402
|
-
|
|
436
|
+
"""Disconnect the client."""
|
|
437
|
+
if self._mqtt_client.is_connected():
|
|
403
438
|
self._mqtt_client.disconnect()
|
|
404
439
|
|
|
405
440
|
|
|
406
441
|
class LGHorizonBox:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
442
|
+
"""The LGHorizon box."""
|
|
443
|
+
|
|
444
|
+
device_id: str = None
|
|
445
|
+
hashed_cpe_id: str = None
|
|
446
|
+
device_friendly_name: str = None
|
|
410
447
|
state: str = None
|
|
411
448
|
playing_info: LGHorizonPlayingInfo = None
|
|
412
449
|
manufacturer: str = None
|
|
@@ -427,9 +464,9 @@ class LGHorizonBox:
|
|
|
427
464
|
auth: LGHorizonAuth,
|
|
428
465
|
channels: Dict[str, LGHorizonChannel],
|
|
429
466
|
):
|
|
430
|
-
self.
|
|
431
|
-
self.
|
|
432
|
-
self.
|
|
467
|
+
self.device_id = box_json["deviceId"]
|
|
468
|
+
self.hashed_cpe_id = box_json["hashedCPEId"]
|
|
469
|
+
self.device_friendly_name = box_json["settings"]["deviceFriendlyName"]
|
|
433
470
|
self._mqtt_client = mqtt_client
|
|
434
471
|
self._auth = auth
|
|
435
472
|
self._channels = channels
|
|
@@ -439,20 +476,23 @@ class LGHorizonBox:
|
|
|
439
476
|
self.model = platform_type["model"]
|
|
440
477
|
|
|
441
478
|
def update_channels(self, channels: Dict[str, LGHorizonChannel]):
|
|
479
|
+
"""Update the channels list."""
|
|
442
480
|
self._channels = channels
|
|
443
481
|
|
|
444
482
|
def register_mqtt(self) -> None:
|
|
483
|
+
"""Register the mqtt connection."""
|
|
445
484
|
if not self._mqtt_client.is_connected:
|
|
446
485
|
raise Exception("MQTT client not connected.")
|
|
447
|
-
topic = f"{self._auth.
|
|
486
|
+
topic = f"{self._auth.household_id}/{self._mqtt_client.client_id}/status"
|
|
448
487
|
payload = {
|
|
449
|
-
"source": self._mqtt_client.
|
|
488
|
+
"source": self._mqtt_client.client_id,
|
|
450
489
|
"state": ONLINE_RUNNING,
|
|
451
490
|
"deviceType": "HGO",
|
|
452
491
|
}
|
|
453
492
|
self._mqtt_client.publish_message(topic, json.dumps(payload))
|
|
454
493
|
|
|
455
494
|
def set_callback(self, change_callback: Callable) -> None:
|
|
495
|
+
"""Set a callback function."""
|
|
456
496
|
self._change_callback = change_callback
|
|
457
497
|
|
|
458
498
|
def update_state(self, payload):
|
|
@@ -464,25 +504,27 @@ class LGHorizonBox:
|
|
|
464
504
|
if state == ONLINE_STANDBY:
|
|
465
505
|
self.playing_info.reset()
|
|
466
506
|
if self._change_callback:
|
|
467
|
-
self._change_callback(self.
|
|
507
|
+
self._change_callback(self.device_id)
|
|
468
508
|
else:
|
|
469
509
|
self._request_settop_box_state()
|
|
470
510
|
self._request_settop_box_recording_capacity()
|
|
471
511
|
|
|
472
512
|
def update_recording_capacity(self, payload) -> None:
|
|
473
|
-
|
|
513
|
+
"""Updates the recording capacity."""
|
|
514
|
+
if "CPE.capacity" not in payload or "used" not in payload:
|
|
474
515
|
return
|
|
475
516
|
self.recording_capacity = payload["used"]
|
|
476
517
|
|
|
477
518
|
def update_with_replay_event(
|
|
478
519
|
self, source_type: str, event: LGHorizonReplayEvent, channel: LGHorizonChannel
|
|
479
520
|
) -> None:
|
|
521
|
+
"""Update box with replay event."""
|
|
480
522
|
self.playing_info.set_source_type(source_type)
|
|
481
523
|
self.playing_info.set_channel(channel.id)
|
|
482
524
|
self.playing_info.set_channel_title(channel.title)
|
|
483
525
|
title = event.title
|
|
484
|
-
if event.
|
|
485
|
-
title += f": {event.
|
|
526
|
+
if event.episode_name:
|
|
527
|
+
title += f": {event.episode_name}"
|
|
486
528
|
self.playing_info.set_title(title)
|
|
487
529
|
self.playing_info.set_image(channel.stream_image)
|
|
488
530
|
self.playing_info.reset_progress()
|
|
@@ -498,6 +540,7 @@ class LGHorizonBox:
|
|
|
498
540
|
last_speed_change: float,
|
|
499
541
|
relative_position: float,
|
|
500
542
|
) -> None:
|
|
543
|
+
"""Update box with recording."""
|
|
501
544
|
self.playing_info.set_source_type(source_type)
|
|
502
545
|
self.playing_info.set_channel(channel.id)
|
|
503
546
|
self.playing_info.set_channel_title(channel.title)
|
|
@@ -519,6 +562,7 @@ class LGHorizonBox:
|
|
|
519
562
|
last_speed_change: float,
|
|
520
563
|
relative_position: float,
|
|
521
564
|
) -> None:
|
|
565
|
+
"""Update box with vod."""
|
|
522
566
|
self.playing_info.set_source_type(source_type)
|
|
523
567
|
self.playing_info.set_channel(None)
|
|
524
568
|
self.playing_info.set_channel_title(None)
|
|
@@ -531,6 +575,7 @@ class LGHorizonBox:
|
|
|
531
575
|
self._trigger_callback()
|
|
532
576
|
|
|
533
577
|
def update_with_app(self, source_type: str, app: LGHorizonApp) -> None:
|
|
578
|
+
"""Update box with app."""
|
|
534
579
|
self.playing_info.set_source_type(source_type)
|
|
535
580
|
self.playing_info.set_channel(None)
|
|
536
581
|
self.playing_info.set_channel_title(app.title)
|
|
@@ -541,8 +586,8 @@ class LGHorizonBox:
|
|
|
541
586
|
|
|
542
587
|
def _trigger_callback(self):
|
|
543
588
|
if self._change_callback:
|
|
544
|
-
_logger.debug(
|
|
545
|
-
self._change_callback(self.
|
|
589
|
+
_logger.debug("Callback called from box %s", self.device_id)
|
|
590
|
+
self._change_callback(self.device_id)
|
|
546
591
|
|
|
547
592
|
def turn_on(self) -> None:
|
|
548
593
|
"""Turn the settop box on."""
|
|
@@ -620,10 +665,10 @@ class LGHorizonBox:
|
|
|
620
665
|
)
|
|
621
666
|
|
|
622
667
|
self._mqtt_client.publish_message(
|
|
623
|
-
f"{self._auth.
|
|
668
|
+
f"{self._auth.household_id}/{self.device_id}", payload
|
|
624
669
|
)
|
|
625
670
|
|
|
626
|
-
def play_recording(self,
|
|
671
|
+
def play_recording(self, recording_id):
|
|
627
672
|
"""Play recording."""
|
|
628
673
|
payload = (
|
|
629
674
|
'{"id":"'
|
|
@@ -632,11 +677,11 @@ class LGHorizonBox:
|
|
|
632
677
|
+ self._mqtt_client.clientId
|
|
633
678
|
+ '","friendlyDeviceName":"Home Assistant"},'
|
|
634
679
|
+ '"status":{"sourceType":"nDVR","source":{"recordingId":"'
|
|
635
|
-
+
|
|
680
|
+
+ recording_id
|
|
636
681
|
+ '"},"relativePosition":0}}'
|
|
637
682
|
)
|
|
638
683
|
self._mqtt_client.publish_message(
|
|
639
|
-
f"{self._auth.
|
|
684
|
+
f"{self._auth.household_id}/{self.device_id}", payload
|
|
640
685
|
)
|
|
641
686
|
|
|
642
687
|
def send_key_to_box(self, key: str) -> None:
|
|
@@ -647,7 +692,7 @@ class LGHorizonBox:
|
|
|
647
692
|
+ '","eventType":"keyDownUp"}}'
|
|
648
693
|
)
|
|
649
694
|
self._mqtt_client.publish_message(
|
|
650
|
-
f"{self._auth.
|
|
695
|
+
f"{self._auth.household_id}/{self.device_id}", payload
|
|
651
696
|
)
|
|
652
697
|
|
|
653
698
|
def _set_unknown_channel_info(self) -> None:
|
|
@@ -661,29 +706,31 @@ class LGHorizonBox:
|
|
|
661
706
|
|
|
662
707
|
def _request_settop_box_state(self) -> None:
|
|
663
708
|
"""Send mqtt message to receive state from settop box."""
|
|
664
|
-
topic = f"{self._auth.
|
|
709
|
+
topic = f"{self._auth.household_id}/{self.device_id}"
|
|
665
710
|
payload = {
|
|
666
711
|
"id": make_id(8),
|
|
667
712
|
"type": "CPE.getUiStatus",
|
|
668
|
-
"source": self._mqtt_client.
|
|
713
|
+
"source": self._mqtt_client.client_id,
|
|
669
714
|
}
|
|
670
715
|
self._mqtt_client.publish_message(topic, json.dumps(payload))
|
|
671
716
|
|
|
672
717
|
def _request_settop_box_recording_capacity(self) -> None:
|
|
673
718
|
"""Send mqtt message to receive state from settop box."""
|
|
674
|
-
topic = f"{self._auth.
|
|
719
|
+
topic = f"{self._auth.household_id}/{self.device_id}"
|
|
675
720
|
payload = {
|
|
676
721
|
"id": make_id(8),
|
|
677
722
|
"type": "CPE.capacity",
|
|
678
|
-
"source": self._mqtt_client.
|
|
723
|
+
"source": self._mqtt_client.client_id,
|
|
679
724
|
}
|
|
680
725
|
self._mqtt_client.publish_message(topic, json.dumps(payload))
|
|
681
726
|
|
|
682
727
|
|
|
683
728
|
class LGHorizonProfile:
|
|
729
|
+
"""LGHorizon profile."""
|
|
730
|
+
|
|
684
731
|
profile_id: str = None
|
|
685
732
|
name: str = None
|
|
686
|
-
favorite_channels: [] = None
|
|
733
|
+
favorite_channels: list[str] = None
|
|
687
734
|
|
|
688
735
|
def __init__(self, json_payload):
|
|
689
736
|
self.profile_id = json_payload["profileId"]
|
|
@@ -692,18 +739,20 @@ class LGHorizonProfile:
|
|
|
692
739
|
|
|
693
740
|
|
|
694
741
|
class LGHorizonCustomer:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
742
|
+
"""LGHorizon customer"""
|
|
743
|
+
|
|
744
|
+
customer_id: str = None
|
|
745
|
+
hashed_customer_id: str = None
|
|
746
|
+
country_id: str = None
|
|
747
|
+
city_id: int = 0
|
|
748
|
+
settop_boxes: list[str] = None
|
|
700
749
|
profiles: Dict[str, LGHorizonProfile] = {}
|
|
701
750
|
|
|
702
751
|
def __init__(self, json_payload):
|
|
703
|
-
self.
|
|
704
|
-
self.
|
|
705
|
-
self.
|
|
706
|
-
self.
|
|
752
|
+
self.customer_id = json_payload["customerId"]
|
|
753
|
+
self.hashed_customer_id = json_payload["hashedCustomerId"]
|
|
754
|
+
self.country_id = json_payload["countryId"]
|
|
755
|
+
self.city_id = json_payload["cityId"]
|
|
707
756
|
if "assignedDevices" in json_payload:
|
|
708
757
|
self.settop_boxes = json_payload["assignedDevices"]
|
|
709
758
|
if "profiles" in json_payload:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lghorizon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0b2
|
|
4
4
|
Summary: Python client for Liberty Global Horizon settop boxes
|
|
5
5
|
Home-page: https://github.com/sholofly/LGHorizon-python
|
|
6
6
|
Author: Rudolf Offereins
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: paho-mqtt
|
|
23
|
+
Requires-Dist: paho-mqtt >=2.0.0
|
|
24
24
|
Requires-Dist: requests >=2.22.0
|
|
25
25
|
Requires-Dist: backoff >=1.9.0
|
|
26
26
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
lghorizon/__init__.py,sha256=GrctR8IIj0G7Sps_N_YjvKsrvMcbhp3O0KxE7yi-_R8,991
|
|
2
|
+
lghorizon/const.py,sha256=8P-4cwRRg0wKtrzr5DTOuGb618HKjGRPMnIfmc6-VSM,4700
|
|
3
|
+
lghorizon/exceptions.py,sha256=-6v55KDTogBldGAg1wV9Mrxm5L5BsaVguhBgVMOeJHk,404
|
|
4
|
+
lghorizon/helpers.py,sha256=W7ppV9MJ1MyXH_49GliNtiu_kA7h1KRzMdmRdrC5kaw,266
|
|
5
|
+
lghorizon/lghorizon_api.py,sha256=xl2ISDPgkuLrdZtxXIBfBYjg2JfKIwBMY-BbMMqmmU0,22343
|
|
6
|
+
lghorizon/models.py,sha256=NSgQd2Zb-YE8VkbhJP6g5mHKMGEBFQiIB1DYqa7IhSI,25990
|
|
7
|
+
lghorizon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
lghorizon-0.8.0b2.dist-info/LICENSE,sha256=6Dh2tur1gMX3r3rITjVwUONBEJxyyPZDY8p6DZXtimE,1059
|
|
9
|
+
lghorizon-0.8.0b2.dist-info/METADATA,sha256=CHR9mTAzm2v4nsBDFxdaVjq0il3Da_T57UD9pY3vpYI,1040
|
|
10
|
+
lghorizon-0.8.0b2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
11
|
+
lghorizon-0.8.0b2.dist-info/top_level.txt,sha256=usii76_AxGfPI6gjrrh-NyZxcQQuF1B8_Q9kd7sID8Q,10
|
|
12
|
+
lghorizon-0.8.0b2.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
lghorizon/__init__.py,sha256=XqePG0hj8NnudOcfCtEWqPsWP5l2headzrQhkfuDlv8,544
|
|
2
|
-
lghorizon/const.py,sha256=8P-4cwRRg0wKtrzr5DTOuGb618HKjGRPMnIfmc6-VSM,4700
|
|
3
|
-
lghorizon/exceptions.py,sha256=-6v55KDTogBldGAg1wV9Mrxm5L5BsaVguhBgVMOeJHk,404
|
|
4
|
-
lghorizon/helpers.py,sha256=ZWpi7B3hBvwGV02KWQQHVyj7FLLUDtIvKc-Iqsj5VHA,263
|
|
5
|
-
lghorizon/lghorizon_api.py,sha256=ZrHHp-fSYLs566_KKi10n52P6T4vlaRyPh50UJqZWno,21993
|
|
6
|
-
lghorizon/models.py,sha256=S_Pq8z9PpaN10b17aoi2j-mCIdZf-TK411pBGRPDkvc,24542
|
|
7
|
-
lghorizon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
lghorizon-0.7.5b2.dist-info/LICENSE,sha256=6Dh2tur1gMX3r3rITjVwUONBEJxyyPZDY8p6DZXtimE,1059
|
|
9
|
-
lghorizon-0.7.5b2.dist-info/METADATA,sha256=ftEhgIKdN1Nq257eTBw8-DPhqXHquorzEpl2IJtknJU,1039
|
|
10
|
-
lghorizon-0.7.5b2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
11
|
-
lghorizon-0.7.5b2.dist-info/top_level.txt,sha256=usii76_AxGfPI6gjrrh-NyZxcQQuF1B8_Q9kd7sID8Q,10
|
|
12
|
-
lghorizon-0.7.5b2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|