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.
Files changed (32) hide show
  1. {lghorizon-0.7.1 → lghorizon-0.7.3}/PKG-INFO +1 -1
  2. lghorizon-0.7.3/lghorizon/__init__.py +23 -0
  3. lghorizon-0.7.3/lghorizon/exceptions.py +17 -0
  4. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/lghorizon_api.py +74 -24
  5. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/models.py +21 -3
  6. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/PKG-INFO +1 -1
  7. lghorizon-0.7.3/secrets_stub.json +7 -0
  8. {lghorizon-0.7.1 → lghorizon-0.7.3}/test.py +6 -1
  9. lghorizon-0.7.1/lghorizon/__init__.py +0 -5
  10. lghorizon-0.7.1/lghorizon/exceptions.py +0 -11
  11. lghorizon-0.7.1/secrets_stub.json +0 -5
  12. {lghorizon-0.7.1 → lghorizon-0.7.3}/.coverage +0 -0
  13. {lghorizon-0.7.1 → lghorizon-0.7.3}/.flake8 +0 -0
  14. {lghorizon-0.7.1 → lghorizon-0.7.3}/.github/workflows/build-on-pr.yml +0 -0
  15. {lghorizon-0.7.1 → lghorizon-0.7.3}/.github/workflows/publish-to-pypi.yml +0 -0
  16. {lghorizon-0.7.1 → lghorizon-0.7.3}/.gitignore +0 -0
  17. {lghorizon-0.7.1 → lghorizon-0.7.3}/LICENSE +0 -0
  18. {lghorizon-0.7.1 → lghorizon-0.7.3}/README.md +0 -0
  19. {lghorizon-0.7.1 → lghorizon-0.7.3}/instructions.txt +0 -0
  20. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/const.py +0 -0
  21. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/helpers.py +0 -0
  22. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon/py.typed +0 -0
  23. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/SOURCES.txt +0 -0
  24. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/dependency_links.txt +0 -0
  25. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/not-zip-safe +0 -0
  26. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/requires.txt +0 -0
  27. {lghorizon-0.7.1 → lghorizon-0.7.3}/lghorizon.egg-info/top_level.txt +0 -0
  28. {lghorizon-0.7.1 → lghorizon-0.7.3}/lib64 +0 -0
  29. {lghorizon-0.7.1 → lghorizon-0.7.3}/pyvenv.cfg +0 -0
  30. {lghorizon-0.7.1 → lghorizon-0.7.3}/renovate.json +0 -0
  31. {lghorizon-0.7.1 → lghorizon-0.7.3}/setup.cfg +0 -0
  32. {lghorizon-0.7.1 → lghorizon-0.7.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lghorizon
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -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 LGHorizonApiUnauthorizedError, LGHorizonApiConnectionError
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
- _customer: LGHorizonCustomer = None
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, BaseException, jitter=None, max_time=600, logger=_logger
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 "source" in message:
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._customer.cityId}"
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
- _logger.info("Get personalisation info...")
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 not "assignedDevices" in personalisation_result:
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 personalisation_result["assignedDevices"]:
405
+ for device in self.customer.settop_boxes:
378
406
  platform_type = device["platformType"]
379
- if not platform_type in _supported_platforms:
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
- platformType = self._country_settings["platform_types"][platform_type]
413
+ platform_type = self._country_settings["platform_types"][platform_type]
386
414
  else:
387
- platformType = None
415
+ platform_type = None
388
416
  box = LGHorizonBox(
389
- device, platformType, self._mqttClient, self._auth, self._channels
417
+ device, platform_type, self._mqttClient, self._auth, self._channels
390
418
  )
391
419
  self.settop_boxes[box.deviceId] = box
392
- _logger.info(f"Box {box.deviceId} registered...")
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._customer.cityId}&language={self._country_settings['language']}&productClass=Orion-DASH"
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: Dict[str, LGHorizonBox] = None
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 not "assignedDevices" in json_payload:
693
- return
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lghorizon
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -0,0 +1,7 @@
1
+ {
2
+ "username": "your_username",
3
+ "password": "your_password",
4
+ "country": "your_country_code",
5
+ "profile_id":"your_profile_id"
6
+
7
+ }
@@ -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 = 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
-
@@ -1,5 +0,0 @@
1
- {
2
- "username": "your_username",
3
- "password": "your_password",
4
- "country": "your_country_code"
5
- }
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