lghorizon 0.7.1__tar.gz → 0.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lghorizon-0.7.1 → lghorizon-0.7.3}/PKG-INFO +1 -1
- lghorizon-0.7.3/lghorizon/__init__.py +23 -0
- lghorizon-0.7.3/lghorizon/exceptions.py +17 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/lghorizon_api.py +74 -24
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/models.py +21 -3
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/PKG-INFO +1 -1
- lghorizon-0.7.3/secrets_stub.json +7 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/test.py +6 -1
- lghorizon-0.7.1/lghorizon/__init__.py +0 -5
- lghorizon-0.7.1/lghorizon/exceptions.py +0 -11
- lghorizon-0.7.1/secrets_stub.json +0 -5
- {lghorizon-0.7.1 → lghorizon-0.7.3}/.coverage +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/.flake8 +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/.github/workflows/build-on-pr.yml +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/.gitignore +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/LICENSE +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/README.md +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/instructions.txt +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/const.py +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/helpers.py +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/py.typed +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/SOURCES.txt +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/dependency_links.txt +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/not-zip-safe +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/requires.txt +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/top_level.txt +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/lib64 +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/pyvenv.cfg +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/renovate.json +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/setup.cfg +0 -0
- {lghorizon-0.7.1 → lghorizon-0.7.3}/setup.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Python client for LG Horizon."""
|
|
2
|
+
|
|
3
|
+
from .lghorizon_api import LGHorizonApi
|
|
4
|
+
from .models import (
|
|
5
|
+
LGHorizonBox,
|
|
6
|
+
LGHorizonRecordingListSeasonShow,
|
|
7
|
+
LGHorizonRecordingSingle,
|
|
8
|
+
LGHorizonRecordingShow,
|
|
9
|
+
LGHorizonRecordingEpisode,
|
|
10
|
+
LGHorizonCustomer,
|
|
11
|
+
)
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
LGHorizonApiUnauthorizedError,
|
|
14
|
+
LGHorizonApiConnectionError,
|
|
15
|
+
LGHorizonApiLockedError,
|
|
16
|
+
)
|
|
17
|
+
from .const import (
|
|
18
|
+
ONLINE_RUNNING,
|
|
19
|
+
ONLINE_STANDBY,
|
|
20
|
+
RECORDING_TYPE_SHOW,
|
|
21
|
+
RECORDING_TYPE_SEASON,
|
|
22
|
+
RECORDING_TYPE_SINGLE,
|
|
23
|
+
) # noqa
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Exceptions for the LGHorizon API."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LGHorizonApiError(Exception):
|
|
5
|
+
"""Generic LGHorizon exception."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LGHorizonApiConnectionError(LGHorizonApiError):
|
|
9
|
+
"""Generic LGHorizon exception."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LGHorizonApiUnauthorizedError(Exception):
|
|
13
|
+
"""Generic LGHorizon exception."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LGHorizonApiLockedError(LGHorizonApiUnauthorizedError):
|
|
17
|
+
"""Generic LGHorizon exception."""
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
import json
|
|
5
5
|
import sys, traceback
|
|
6
|
-
from .exceptions import
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
LGHorizonApiUnauthorizedError,
|
|
8
|
+
LGHorizonApiConnectionError,
|
|
9
|
+
LGHorizonApiLockedError,
|
|
10
|
+
)
|
|
7
11
|
import backoff
|
|
8
12
|
from requests import Session, exceptions as request_exceptions
|
|
9
13
|
from paho.mqtt.client import WebsocketConnectionError
|
|
@@ -35,7 +39,7 @@ from .const import (
|
|
|
35
39
|
RECORDING_TYPE_SEASON,
|
|
36
40
|
RECORDING_TYPE_SHOW,
|
|
37
41
|
)
|
|
38
|
-
from typing import Any, Dict, List
|
|
42
|
+
from typing import Any, Callable, Dict, List
|
|
39
43
|
|
|
40
44
|
_logger = logging.getLogger(__name__)
|
|
41
45
|
_supported_platforms = ["EOS", "EOS2", "HORIZON", "APOLLO"]
|
|
@@ -47,7 +51,7 @@ class LGHorizonApi:
|
|
|
47
51
|
_auth: LGHorizonAuth = None
|
|
48
52
|
_session: Session = None
|
|
49
53
|
settop_boxes: Dict[str, LGHorizonBox] = None
|
|
50
|
-
|
|
54
|
+
customer: LGHorizonCustomer = None
|
|
51
55
|
_mqttClient: LGHorizonMqttClient = None
|
|
52
56
|
_channels: Dict[str, LGHorizonChannel] = None
|
|
53
57
|
_country_settings = None
|
|
@@ -56,6 +60,8 @@ class LGHorizonApi:
|
|
|
56
60
|
_entitlements: List[str] = None
|
|
57
61
|
_identifier: str = None
|
|
58
62
|
_config: str = None
|
|
63
|
+
_refresh_callback: Callable = None
|
|
64
|
+
_profile_id: str = None
|
|
59
65
|
|
|
60
66
|
def __init__(
|
|
61
67
|
self,
|
|
@@ -64,6 +70,7 @@ class LGHorizonApi:
|
|
|
64
70
|
country_code: str = "nl",
|
|
65
71
|
identifier: str = None,
|
|
66
72
|
refresh_token=None,
|
|
73
|
+
profile_id=None,
|
|
67
74
|
) -> None:
|
|
68
75
|
"""Create LGHorizon API."""
|
|
69
76
|
self.username = username
|
|
@@ -77,10 +84,8 @@ class LGHorizonApi:
|
|
|
77
84
|
self._channels = {}
|
|
78
85
|
self._entitlements = []
|
|
79
86
|
self._identifier = identifier
|
|
87
|
+
self._profile_id = profile_id
|
|
80
88
|
|
|
81
|
-
@backoff.on_exception(
|
|
82
|
-
backoff.expo, LGHorizonApiConnectionError, max_tries=3, logger=_logger
|
|
83
|
-
)
|
|
84
89
|
def _authorize(self) -> None:
|
|
85
90
|
ctry_code = self._country_code[0:2]
|
|
86
91
|
if ctry_code == "be":
|
|
@@ -107,6 +112,8 @@ class LGHorizonApi:
|
|
|
107
112
|
error = error_json["error"]
|
|
108
113
|
if error and error["statusCode"] == 97401:
|
|
109
114
|
raise LGHorizonApiUnauthorizedError("Invalid credentials")
|
|
115
|
+
elif error and error["statusCode"] == 97117:
|
|
116
|
+
raise LGHorizonApiLockedError("Account locked")
|
|
110
117
|
elif error:
|
|
111
118
|
raise LGHorizonApiConnectionError(error["message"])
|
|
112
119
|
else:
|
|
@@ -146,8 +153,15 @@ class LGHorizonApi:
|
|
|
146
153
|
self._auth.fill(auth_response.json())
|
|
147
154
|
self.refresh_token = self._auth.refreshToken
|
|
148
155
|
self._session.cookies["ACCESSTOKEN"] = self._auth.accessToken
|
|
156
|
+
|
|
157
|
+
if self._refresh_callback:
|
|
158
|
+
self._refresh_callback()
|
|
159
|
+
|
|
149
160
|
_logger.debug("Authorization succeeded")
|
|
150
161
|
|
|
162
|
+
def set_callback(self, refresh_callback: Callable) -> None:
|
|
163
|
+
self._refresh_callback = refresh_callback
|
|
164
|
+
|
|
151
165
|
def authorize_telenet(self):
|
|
152
166
|
try:
|
|
153
167
|
login_session = Session()
|
|
@@ -225,7 +239,14 @@ class LGHorizonApi:
|
|
|
225
239
|
_logger.debug(f"MQTT token: {self._auth.mqttToken}")
|
|
226
240
|
|
|
227
241
|
@backoff.on_exception(
|
|
228
|
-
backoff.expo,
|
|
242
|
+
backoff.expo,
|
|
243
|
+
BaseException,
|
|
244
|
+
jitter=None,
|
|
245
|
+
max_tries=3,
|
|
246
|
+
logger=_logger,
|
|
247
|
+
giveup=lambda e: isinstance(
|
|
248
|
+
e, (LGHorizonApiLockedError, LGHorizonApiUnauthorizedError)
|
|
249
|
+
),
|
|
229
250
|
)
|
|
230
251
|
def connect(self) -> None:
|
|
231
252
|
self._config = self._get_config(self._country_code)
|
|
@@ -238,13 +259,14 @@ class LGHorizonApi:
|
|
|
238
259
|
self._on_mqtt_connected,
|
|
239
260
|
self._on_mqtt_message,
|
|
240
261
|
)
|
|
262
|
+
|
|
241
263
|
self._register_customer_and_boxes()
|
|
242
264
|
self._mqttClient.connect()
|
|
243
265
|
|
|
244
266
|
def disconnect(self):
|
|
245
267
|
"""Disconnect."""
|
|
246
268
|
_logger.debug("Disconnect from API")
|
|
247
|
-
if not self._mqttClient.is_connected:
|
|
269
|
+
if not self._mqttClient or not self._mqttClient.is_connected:
|
|
248
270
|
return
|
|
249
271
|
self._mqttClient.disconnect()
|
|
250
272
|
|
|
@@ -255,8 +277,18 @@ class LGHorizonApi:
|
|
|
255
277
|
box.register_mqtt()
|
|
256
278
|
|
|
257
279
|
def _on_mqtt_message(self, message: str, topic: str) -> None:
|
|
258
|
-
if "
|
|
280
|
+
if "action" in message and message["action"] == "OPS.getProfilesUpdate":
|
|
281
|
+
self._update_customer()
|
|
282
|
+
self._channels.clear()
|
|
283
|
+
self._get_channels()
|
|
284
|
+
box: LGHorizonBox
|
|
285
|
+
for box in self.settop_boxes.values():
|
|
286
|
+
box.update_channels(self._channels)
|
|
287
|
+
elif "source" in message:
|
|
259
288
|
deviceId = message["source"]
|
|
289
|
+
if not isinstance(deviceId, str):
|
|
290
|
+
_logger.debug("ignoring message - not a string")
|
|
291
|
+
return
|
|
260
292
|
if not deviceId in self.settop_boxes.keys():
|
|
261
293
|
return
|
|
262
294
|
try:
|
|
@@ -264,6 +296,7 @@ class LGHorizonApi:
|
|
|
264
296
|
self.settop_boxes[deviceId].update_state(message)
|
|
265
297
|
if "status" in message:
|
|
266
298
|
self._handle_box_update(deviceId, message)
|
|
299
|
+
|
|
267
300
|
except Exception:
|
|
268
301
|
_logger.exception("Could not handle status message")
|
|
269
302
|
_logger.warning(f"Full message: {str(message)}")
|
|
@@ -335,7 +368,7 @@ class LGHorizonApi:
|
|
|
335
368
|
last_speed_change_time = playerState["lastSpeedChangeTime"]
|
|
336
369
|
relative_position = playerState["relativePosition"]
|
|
337
370
|
raw_vod = self._do_api_call(
|
|
338
|
-
f"{self._config['vodService']['URL']}/v2/detailscreen/{titleId}?language={self._country_settings['language']}&profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&cityId={self.
|
|
371
|
+
f"{self._config['vodService']['URL']}/v2/detailscreen/{titleId}?language={self._country_settings['language']}&profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&cityId={self.customer.cityId}"
|
|
339
372
|
)
|
|
340
373
|
vod = LGHorizonVod(raw_vod)
|
|
341
374
|
self.settop_boxes[deviceId].update_with_vod(
|
|
@@ -363,40 +396,50 @@ class LGHorizonApi:
|
|
|
363
396
|
return json_response
|
|
364
397
|
|
|
365
398
|
def _register_customer_and_boxes(self):
|
|
366
|
-
|
|
367
|
-
personalisation_result = self._do_api_call(
|
|
368
|
-
f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.householdId}?with=profiles%2Cdevices"
|
|
369
|
-
)
|
|
370
|
-
_logger.debug(f"Personalisation result: {personalisation_result}")
|
|
371
|
-
self._customer = LGHorizonCustomer(personalisation_result)
|
|
399
|
+
self._update_customer()
|
|
372
400
|
self._get_channels()
|
|
373
|
-
if
|
|
401
|
+
if len(self.customer.settop_boxes) == 0:
|
|
374
402
|
_logger.warning("No boxes found.")
|
|
375
403
|
return
|
|
376
404
|
_logger.info("Registering boxes")
|
|
377
|
-
for device in
|
|
405
|
+
for device in self.customer.settop_boxes:
|
|
378
406
|
platform_type = device["platformType"]
|
|
379
|
-
if not
|
|
407
|
+
if platform_type not in _supported_platforms:
|
|
380
408
|
continue
|
|
381
409
|
if (
|
|
382
410
|
"platform_types" in self._country_settings
|
|
383
411
|
and platform_type in self._country_settings["platform_types"]
|
|
384
412
|
):
|
|
385
|
-
|
|
413
|
+
platform_type = self._country_settings["platform_types"][platform_type]
|
|
386
414
|
else:
|
|
387
|
-
|
|
415
|
+
platform_type = None
|
|
388
416
|
box = LGHorizonBox(
|
|
389
|
-
device,
|
|
417
|
+
device, platform_type, self._mqttClient, self._auth, self._channels
|
|
390
418
|
)
|
|
391
419
|
self.settop_boxes[box.deviceId] = box
|
|
392
|
-
_logger.info(
|
|
420
|
+
_logger.info("Box %s registered...", box.deviceId)
|
|
421
|
+
|
|
422
|
+
def _update_customer(self):
|
|
423
|
+
_logger.info("Get customer data")
|
|
424
|
+
personalisation_result = self._do_api_call(
|
|
425
|
+
f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.householdId}?with=profiles%2Cdevices"
|
|
426
|
+
)
|
|
427
|
+
_logger.debug("Personalisation result: %s ", personalisation_result)
|
|
428
|
+
self.customer = LGHorizonCustomer(personalisation_result)
|
|
393
429
|
|
|
394
430
|
def _get_channels(self):
|
|
395
431
|
self._update_entitlements()
|
|
396
432
|
_logger.info("Retrieving channels...")
|
|
397
433
|
channels_result = self._do_api_call(
|
|
398
|
-
f"{self._config['linearService']['URL']}/v2/channels?cityId={self.
|
|
434
|
+
f"{self._config['linearService']['URL']}/v2/channels?cityId={self.customer.cityId}&language={self._country_settings['language']}&productClass=Orion-DASH"
|
|
399
435
|
)
|
|
436
|
+
profile_channels = []
|
|
437
|
+
if self._profile_id and self._profile_id in self.customer.profiles:
|
|
438
|
+
profile_channels = self.customer.profiles[
|
|
439
|
+
self._profile_id
|
|
440
|
+
].favorite_channels
|
|
441
|
+
|
|
442
|
+
has_profile_channels = len(profile_channels) > 0
|
|
400
443
|
for channel in channels_result:
|
|
401
444
|
if "isRadio" in channel and channel["isRadio"]:
|
|
402
445
|
continue
|
|
@@ -406,6 +449,9 @@ class LGHorizonApi:
|
|
|
406
449
|
if len(common_entitlements) == 0:
|
|
407
450
|
continue
|
|
408
451
|
channel_id = channel["id"]
|
|
452
|
+
if has_profile_channels and channel_id not in profile_channels:
|
|
453
|
+
continue
|
|
454
|
+
|
|
409
455
|
self._channels[channel_id] = LGHorizonChannel(channel)
|
|
410
456
|
_logger.info(f"{len(self._channels)} retrieved.")
|
|
411
457
|
|
|
@@ -420,6 +466,10 @@ class LGHorizonApi:
|
|
|
420
466
|
|
|
421
467
|
def get_recording_capacity(self) -> int:
|
|
422
468
|
"""Returns remaining recording capacity"""
|
|
469
|
+
ctry_code = self._country_code[0:2]
|
|
470
|
+
if ctry_code == "gb":
|
|
471
|
+
_logger.debug("GB: not supported")
|
|
472
|
+
return None
|
|
423
473
|
try:
|
|
424
474
|
_logger.info("Retrieving recordingcapacity...")
|
|
425
475
|
quota_content = self._do_api_call(
|
|
@@ -438,6 +438,9 @@ class LGHorizonBox:
|
|
|
438
438
|
self.manufacturer = platform_type["manufacturer"]
|
|
439
439
|
self.model = platform_type["model"]
|
|
440
440
|
|
|
441
|
+
def update_channels(self, channels: Dict[str, LGHorizonChannel]):
|
|
442
|
+
self._channels = channels
|
|
443
|
+
|
|
441
444
|
def register_mqtt(self) -> None:
|
|
442
445
|
if not self._mqtt_client.is_connected:
|
|
443
446
|
raise Exception("MQTT client not connected.")
|
|
@@ -677,17 +680,32 @@ class LGHorizonBox:
|
|
|
677
680
|
self._mqtt_client.publish_message(topic, json.dumps(payload))
|
|
678
681
|
|
|
679
682
|
|
|
683
|
+
class LGHorizonProfile:
|
|
684
|
+
profile_id: str = None
|
|
685
|
+
name: str = None
|
|
686
|
+
favorite_channels: [] = None
|
|
687
|
+
|
|
688
|
+
def __init__(self, json_payload):
|
|
689
|
+
self.profile_id = json_payload["profileId"]
|
|
690
|
+
self.name = json_payload["name"]
|
|
691
|
+
self.favorite_channels = json_payload["favoriteChannels"]
|
|
692
|
+
|
|
693
|
+
|
|
680
694
|
class LGHorizonCustomer:
|
|
681
695
|
customerId: str = None
|
|
682
696
|
hashedCustomerId: str = None
|
|
683
697
|
countryId: str = None
|
|
684
698
|
cityId: int = 0
|
|
685
|
-
settop_boxes:
|
|
699
|
+
settop_boxes: [] = None
|
|
700
|
+
profiles: Dict[str, LGHorizonProfile] = {}
|
|
686
701
|
|
|
687
702
|
def __init__(self, json_payload):
|
|
688
703
|
self.customerId = json_payload["customerId"]
|
|
689
704
|
self.hashedCustomerId = json_payload["hashedCustomerId"]
|
|
690
705
|
self.countryId = json_payload["countryId"]
|
|
691
706
|
self.cityId = json_payload["cityId"]
|
|
692
|
-
if
|
|
693
|
-
|
|
707
|
+
if "assignedDevices" in json_payload:
|
|
708
|
+
self.settop_boxes = json_payload["assignedDevices"]
|
|
709
|
+
if "profiles" in json_payload:
|
|
710
|
+
for profile in json_payload["profiles"]:
|
|
711
|
+
self.profiles[profile["profileId"]] = LGHorizonProfile(profile)
|
|
@@ -57,12 +57,17 @@ if __name__ == "__main__":
|
|
|
57
57
|
if "refresh_token" in secrets:
|
|
58
58
|
refresh_token = secrets["refresh_token"]
|
|
59
59
|
|
|
60
|
+
profile_id = None
|
|
61
|
+
if "profile_id" in secrets:
|
|
62
|
+
profile_id = secrets["profile_id"]
|
|
63
|
+
|
|
60
64
|
api = LGHorizonApi(
|
|
61
65
|
secrets["username"],
|
|
62
66
|
secrets["password"],
|
|
63
67
|
secrets["country"],
|
|
64
68
|
# identifier="DTV3907048",
|
|
65
|
-
refresh_token
|
|
69
|
+
refresh_token=refresh_token,
|
|
70
|
+
profile_id=profile_id
|
|
66
71
|
)
|
|
67
72
|
api.connect()
|
|
68
73
|
event_loop()
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
"""Python client for LG Horizon."""
|
|
2
|
-
from .lghorizon_api import LGHorizonApi
|
|
3
|
-
from .models import LGHorizonBox, LGHorizonRecordingListSeasonShow, LGHorizonRecordingSingle, LGHorizonRecordingShow, LGHorizonRecordingEpisode
|
|
4
|
-
from .exceptions import LGHorizonApiUnauthorizedError, LGHorizonApiConnectionError
|
|
5
|
-
from .const import ONLINE_RUNNING, ONLINE_STANDBY, RECORDING_TYPE_SHOW, RECORDING_TYPE_SEASON, RECORDING_TYPE_SINGLE# noqa
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Exceptions for the LGHorizon API."""
|
|
2
|
-
|
|
3
|
-
class LGHorizonApiError(Exception):
|
|
4
|
-
"""Generic GeocachingApi exception."""
|
|
5
|
-
|
|
6
|
-
class LGHorizonApiConnectionError(LGHorizonApiError):
|
|
7
|
-
"""Generic GeocachingApi exception."""
|
|
8
|
-
|
|
9
|
-
class LGHorizonApiUnauthorizedError(Exception):
|
|
10
|
-
"""Generic GeocachingApi exception."""
|
|
11
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|