lghorizon 0.7.2__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.2 → 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.2 → lghorizon-0.7.3}/lghorizon/lghorizon_api.py +60 -25
  5. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon/models.py +21 -3
  6. {lghorizon-0.7.2 → 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.2 → lghorizon-0.7.3}/test.py +6 -1
  9. lghorizon-0.7.2/lghorizon/__init__.py +0 -5
  10. lghorizon-0.7.2/lghorizon/exceptions.py +0 -11
  11. lghorizon-0.7.2/secrets_stub.json +0 -5
  12. {lghorizon-0.7.2 → lghorizon-0.7.3}/.coverage +0 -0
  13. {lghorizon-0.7.2 → lghorizon-0.7.3}/.flake8 +0 -0
  14. {lghorizon-0.7.2 → lghorizon-0.7.3}/.github/workflows/build-on-pr.yml +0 -0
  15. {lghorizon-0.7.2 → lghorizon-0.7.3}/.github/workflows/publish-to-pypi.yml +0 -0
  16. {lghorizon-0.7.2 → lghorizon-0.7.3}/.gitignore +0 -0
  17. {lghorizon-0.7.2 → lghorizon-0.7.3}/LICENSE +0 -0
  18. {lghorizon-0.7.2 → lghorizon-0.7.3}/README.md +0 -0
  19. {lghorizon-0.7.2 → lghorizon-0.7.3}/instructions.txt +0 -0
  20. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon/const.py +0 -0
  21. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon/helpers.py +0 -0
  22. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon/py.typed +0 -0
  23. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon.egg-info/SOURCES.txt +0 -0
  24. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon.egg-info/dependency_links.txt +0 -0
  25. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon.egg-info/not-zip-safe +0 -0
  26. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon.egg-info/requires.txt +0 -0
  27. {lghorizon-0.7.2 → lghorizon-0.7.3}/lghorizon.egg-info/top_level.txt +0 -0
  28. {lghorizon-0.7.2 → lghorizon-0.7.3}/lib64 +0 -0
  29. {lghorizon-0.7.2 → lghorizon-0.7.3}/pyvenv.cfg +0 -0
  30. {lghorizon-0.7.2 → lghorizon-0.7.3}/renovate.json +0 -0
  31. {lghorizon-0.7.2 → lghorizon-0.7.3}/setup.cfg +0 -0
  32. {lghorizon-0.7.2 → 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.2
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
@@ -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
@@ -57,6 +61,7 @@ class LGHorizonApi:
57
61
  _identifier: str = None
58
62
  _config: str = None
59
63
  _refresh_callback: Callable = None
64
+ _profile_id: str = None
60
65
 
61
66
  def __init__(
62
67
  self,
@@ -65,6 +70,7 @@ class LGHorizonApi:
65
70
  country_code: str = "nl",
66
71
  identifier: str = None,
67
72
  refresh_token=None,
73
+ profile_id=None,
68
74
  ) -> None:
69
75
  """Create LGHorizon API."""
70
76
  self.username = username
@@ -78,10 +84,8 @@ class LGHorizonApi:
78
84
  self._channels = {}
79
85
  self._entitlements = []
80
86
  self._identifier = identifier
87
+ self._profile_id = profile_id
81
88
 
82
- @backoff.on_exception(
83
- backoff.expo, LGHorizonApiConnectionError, max_tries=3, logger=_logger
84
- )
85
89
  def _authorize(self) -> None:
86
90
  ctry_code = self._country_code[0:2]
87
91
  if ctry_code == "be":
@@ -108,6 +112,8 @@ class LGHorizonApi:
108
112
  error = error_json["error"]
109
113
  if error and error["statusCode"] == 97401:
110
114
  raise LGHorizonApiUnauthorizedError("Invalid credentials")
115
+ elif error and error["statusCode"] == 97117:
116
+ raise LGHorizonApiLockedError("Account locked")
111
117
  elif error:
112
118
  raise LGHorizonApiConnectionError(error["message"])
113
119
  else:
@@ -149,7 +155,7 @@ class LGHorizonApi:
149
155
  self._session.cookies["ACCESSTOKEN"] = self._auth.accessToken
150
156
 
151
157
  if self._refresh_callback:
152
- self._refresh_callback ()
158
+ self._refresh_callback()
153
159
 
154
160
  _logger.debug("Authorization succeeded")
155
161
 
@@ -233,7 +239,14 @@ class LGHorizonApi:
233
239
  _logger.debug(f"MQTT token: {self._auth.mqttToken}")
234
240
 
235
241
  @backoff.on_exception(
236
- 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
+ ),
237
250
  )
238
251
  def connect(self) -> None:
239
252
  self._config = self._get_config(self._country_code)
@@ -246,13 +259,14 @@ class LGHorizonApi:
246
259
  self._on_mqtt_connected,
247
260
  self._on_mqtt_message,
248
261
  )
262
+
249
263
  self._register_customer_and_boxes()
250
264
  self._mqttClient.connect()
251
265
 
252
266
  def disconnect(self):
253
267
  """Disconnect."""
254
268
  _logger.debug("Disconnect from API")
255
- if not self._mqttClient.is_connected:
269
+ if not self._mqttClient or not self._mqttClient.is_connected:
256
270
  return
257
271
  self._mqttClient.disconnect()
258
272
 
@@ -263,9 +277,16 @@ class LGHorizonApi:
263
277
  box.register_mqtt()
264
278
 
265
279
  def _on_mqtt_message(self, message: str, topic: str) -> None:
266
- 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:
267
288
  deviceId = message["source"]
268
- if not isinstance(deviceId,str):
289
+ if not isinstance(deviceId, str):
269
290
  _logger.debug("ignoring message - not a string")
270
291
  return
271
292
  if not deviceId in self.settop_boxes.keys():
@@ -275,6 +296,7 @@ class LGHorizonApi:
275
296
  self.settop_boxes[deviceId].update_state(message)
276
297
  if "status" in message:
277
298
  self._handle_box_update(deviceId, message)
299
+
278
300
  except Exception:
279
301
  _logger.exception("Could not handle status message")
280
302
  _logger.warning(f"Full message: {str(message)}")
@@ -346,7 +368,7 @@ class LGHorizonApi:
346
368
  last_speed_change_time = playerState["lastSpeedChangeTime"]
347
369
  relative_position = playerState["relativePosition"]
348
370
  raw_vod = self._do_api_call(
349
- 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}"
350
372
  )
351
373
  vod = LGHorizonVod(raw_vod)
352
374
  self.settop_boxes[deviceId].update_with_vod(
@@ -374,40 +396,50 @@ class LGHorizonApi:
374
396
  return json_response
375
397
 
376
398
  def _register_customer_and_boxes(self):
377
- _logger.info("Get personalisation info...")
378
- personalisation_result = self._do_api_call(
379
- f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.householdId}?with=profiles%2Cdevices"
380
- )
381
- _logger.debug(f"Personalisation result: {personalisation_result}")
382
- self._customer = LGHorizonCustomer(personalisation_result)
399
+ self._update_customer()
383
400
  self._get_channels()
384
- if not "assignedDevices" in personalisation_result:
401
+ if len(self.customer.settop_boxes) == 0:
385
402
  _logger.warning("No boxes found.")
386
403
  return
387
404
  _logger.info("Registering boxes")
388
- for device in personalisation_result["assignedDevices"]:
405
+ for device in self.customer.settop_boxes:
389
406
  platform_type = device["platformType"]
390
- if not platform_type in _supported_platforms:
407
+ if platform_type not in _supported_platforms:
391
408
  continue
392
409
  if (
393
410
  "platform_types" in self._country_settings
394
411
  and platform_type in self._country_settings["platform_types"]
395
412
  ):
396
- platformType = self._country_settings["platform_types"][platform_type]
413
+ platform_type = self._country_settings["platform_types"][platform_type]
397
414
  else:
398
- platformType = None
415
+ platform_type = None
399
416
  box = LGHorizonBox(
400
- device, platformType, self._mqttClient, self._auth, self._channels
417
+ device, platform_type, self._mqttClient, self._auth, self._channels
401
418
  )
402
419
  self.settop_boxes[box.deviceId] = box
403
- _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)
404
429
 
405
430
  def _get_channels(self):
406
431
  self._update_entitlements()
407
432
  _logger.info("Retrieving channels...")
408
433
  channels_result = self._do_api_call(
409
- 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"
410
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
411
443
  for channel in channels_result:
412
444
  if "isRadio" in channel and channel["isRadio"]:
413
445
  continue
@@ -417,6 +449,9 @@ class LGHorizonApi:
417
449
  if len(common_entitlements) == 0:
418
450
  continue
419
451
  channel_id = channel["id"]
452
+ if has_profile_channels and channel_id not in profile_channels:
453
+ continue
454
+
420
455
  self._channels[channel_id] = LGHorizonChannel(channel)
421
456
  _logger.info(f"{len(self._channels)} retrieved.")
422
457
 
@@ -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.2
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