lghorizon 0.8.0b2__tar.gz → 0.9.0b0__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 (39) hide show
  1. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.github/workflows/build-on-pr.yml +1 -1
  2. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.github/workflows/publish-to-pypi.yml +3 -3
  3. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.gitignore +1 -0
  4. lghorizon-0.9.0b0/.vscode/launch.json +15 -0
  5. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/PKG-INFO +15 -3
  6. lghorizon-0.9.0b0/lghorizon/__init__.py +6 -0
  7. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/const.py +20 -4
  8. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/helpers.py +1 -1
  9. {lghorizon-0.8.0b2/lghorizon → lghorizon-0.9.0b0/lghorizon/legacy}/lghorizon_api.py +1 -73
  10. {lghorizon-0.8.0b2/lghorizon → lghorizon-0.9.0b0/lghorizon/legacy}/models.py +25 -17
  11. lghorizon-0.9.0b0/lghorizon/lghorizon_api.py +270 -0
  12. lghorizon-0.9.0b0/lghorizon/lghorizon_device.py +336 -0
  13. lghorizon-0.9.0b0/lghorizon/lghorizon_device_state_processor.py +301 -0
  14. lghorizon-0.9.0b0/lghorizon/lghorizon_message_factory.py +39 -0
  15. lghorizon-0.9.0b0/lghorizon/lghorizon_models.py +1331 -0
  16. lghorizon-0.9.0b0/lghorizon/lghorizon_mqtt_client.py +123 -0
  17. lghorizon-0.9.0b0/lghorizon/lghorizon_recording_factory.py +41 -0
  18. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/PKG-INFO +15 -3
  19. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/SOURCES.txt +11 -3
  20. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/requires.txt +1 -1
  21. lghorizon-0.9.0b0/main.py +94 -0
  22. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/secrets_stub.json +2 -2
  23. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/setup.py +1 -1
  24. lghorizon-0.8.0b2/lghorizon/__init__.py +0 -41
  25. lghorizon-0.8.0b2/test.py +0 -84
  26. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.coverage +0 -0
  27. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.flake8 +0 -0
  28. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/LICENSE +0 -0
  29. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/README.md +0 -0
  30. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/instructions.txt +0 -0
  31. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/exceptions.py +0 -0
  32. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/py.typed +0 -0
  33. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/dependency_links.txt +0 -0
  34. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/not-zip-safe +0 -0
  35. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/top_level.txt +0 -0
  36. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lib64 +0 -0
  37. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/pyvenv.cfg +0 -0
  38. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/renovate.json +0 -0
  39. {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/setup.cfg +0 -0
@@ -13,7 +13,7 @@ jobs:
13
13
  - name: Set up Python 3.10
14
14
  uses: actions/setup-python@v5
15
15
  with:
16
- python-version: '3.10'
16
+ python-version: '3.13'
17
17
  - name: Install pypa/build
18
18
  run: >-
19
19
  python -m
@@ -7,12 +7,14 @@ jobs:
7
7
  build-n-publish:
8
8
  name: Publish Python 🐍 distribution 📦 to Pypi
9
9
  runs-on: ubuntu-24.04
10
+ permissions:
11
+ id-token: write
10
12
  steps:
11
13
  - uses: actions/checkout@master
12
14
  - name: Set up Python 3.10
13
15
  uses: actions/setup-python@v5
14
16
  with:
15
- python-version: '3.10'
17
+ python-version: '3.13'
16
18
 
17
19
  - name: Install pypa/build
18
20
  run: >-
@@ -29,5 +31,3 @@ jobs:
29
31
  --outdir dist/
30
32
  - name: Publish distribution 📦 to PyPI
31
33
  uses: pypa/gh-action-pypi-publish@release/v1
32
- with:
33
- password: ${{ secrets.PYPI_API_TOKEN }}
@@ -24,3 +24,4 @@ __pycache__/*
24
24
  test/nl.py
25
25
  secrets.json
26
26
  logfile.log
27
+ lghorizon.log
@@ -0,0 +1,15 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "name": "Python Debugger: Debug LGHorizon",
9
+ "type": "debugpy",
10
+ "request": "launch",
11
+ "program": "main.py",
12
+ "console": "integratedTerminal"
13
+ }
14
+ ]
15
+ }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lghorizon
3
- Version: 0.8.0b2
3
+ Version: 0.9.0b0
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,9 +20,21 @@ 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>=2.0.0
23
+ Requires-Dist: paho-mqtt
24
24
  Requires-Dist: requests>=2.22.0
25
25
  Requires-Dist: backoff>=1.9.0
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: keywords
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
26
38
 
27
39
  # LG Horizon Api
28
40
 
@@ -0,0 +1,6 @@
1
+ """Python client for LG Horizon."""
2
+
3
+ from .lghorizon_api import LGHorizonApi
4
+ from .lghorizon_device import LGHorizonDevice
5
+ from .lghorizon_models import *
6
+ from .exceptions import *
@@ -38,11 +38,18 @@ RECORDING_TYPE_SEASON = "season"
38
38
 
39
39
  BE_AUTH_URL = "https://login.prd.telenet.be/openid/login.do"
40
40
 
41
+ PLATFORM_TYPES = {
42
+ "EOS": {"manufacturer": "Arris", "model": "DCX960"},
43
+ "EOS2": {"manufacturer": "HUMAX", "model": "2008C-STB-TN"},
44
+ "HORIZON": {"manufacturer": "Arris", "model": "DCX960"},
45
+ "APOLLO": {"manufacturer": "Arris", "model": "VIP5002W"},
46
+ }
47
+
41
48
  COUNTRY_SETTINGS = {
42
49
  "nl": {
43
50
  "api_url": "https://spark-prod-nl.gnp.cloud.ziggogo.tv",
44
51
  "mqtt_url": "obomsg.prod.nl.horizon.tv",
45
- "use_oauth": False,
52
+ "use_refreshtoken": False,
46
53
  "channels": [
47
54
  {
48
55
  "channelId": "NL_000073_019506",
@@ -76,6 +83,15 @@ COUNTRY_SETTINGS = {
76
83
  "channels": [],
77
84
  "language": "de",
78
85
  },
86
+ "be-basetv": {
87
+ "api_url": "https://spark-prod-be.gnp.cloud.base.tv",
88
+ "channels": [],
89
+ "language": "nl",
90
+ "platform_types": {
91
+ "EOS": {"manufacturer": "Arris", "model": "DCX960"},
92
+ "HORIZON": {"manufacturer": "Arris", "model": "VIP5002W"},
93
+ },
94
+ },
79
95
  "be-nl": {
80
96
  "api_url": "https://spark-prod-be.gnp.cloud.telenet.tv",
81
97
  "oauth_username_fieldname": "j_username",
@@ -97,7 +113,7 @@ COUNTRY_SETTINGS = {
97
113
  },
98
114
  "be-nl-preprod": {
99
115
  "api_url": "https://spark-preprod-be.gnp.cloud.telenet.tv",
100
- "use_oauth": True,
116
+ "use_refreshtoken": True,
101
117
  "oauth_username_fieldname": "j_username",
102
118
  "oauth_password_fieldname": "j_password",
103
119
  "oauth_add_accept_header": False,
@@ -122,13 +138,13 @@ COUNTRY_SETTINGS = {
122
138
  },
123
139
  "ie": {
124
140
  "api_url": "https://spark-prod-ie.gnp.cloud.virginmediatv.ie",
125
- "use_oauth": False,
141
+ "use_refreshtoken": False,
126
142
  "channels": [],
127
143
  "language": "en",
128
144
  },
129
145
  "pl": {
130
146
  "api_url": "https://spark-prod-pl.gnp.cloud.upctv.pl",
131
- "use_oauth": False,
147
+ "use_refreshtoken": False,
132
148
  "channels": [],
133
149
  "language": "pl",
134
150
  "platform_types": {
@@ -3,7 +3,7 @@
3
3
  import random
4
4
 
5
5
 
6
- def make_id(string_length=10):
6
+ async def make_id(string_length=10):
7
7
  """Create an id with given length."""
8
8
  letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
9
9
  return "".join(random.choice(letters) for i in range(string_length))
@@ -93,9 +93,7 @@ class LGHorizonApi:
93
93
 
94
94
  def _authorize(self) -> None:
95
95
  ctry_code = self._country_code[0:2]
96
- if ctry_code == "be":
97
- self._authorize_telenet()
98
- elif ctry_code in ("gb", "ch"):
96
+ if ctry_code in ("gb", "ch", "be"):
99
97
  self._authorize_with_refresh_token()
100
98
  else:
101
99
  self._authorize_default()
@@ -169,76 +167,6 @@ class LGHorizonApi:
169
167
  """Set the refresh callback."""
170
168
  self._refresh_callback = refresh_callback
171
169
 
172
- def _authorize_telenet(self):
173
- """Authorize telenet users."""
174
- try:
175
- login_session = Session()
176
- # Step 1 - Get Authorization data
177
- _logger.debug("Step 1 - Get Authorization data")
178
- auth_url = (
179
- f"{self._country_settings['api_url']}/auth-service/v1/sso/authorization"
180
- )
181
- auth_response = login_session.get(auth_url)
182
- if not auth_response.ok:
183
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
184
- auth_response_json = auth_response.json()
185
- authorization_uri = auth_response_json["authorizationUri"]
186
- authorization_validity_token = auth_response_json["validityToken"]
187
-
188
- # Step 2 - Get Authorization cookie
189
- _logger.debug("Step 2 - Get Authorization cookie")
190
-
191
- auth_cookie_response = login_session.get(authorization_uri)
192
- if not auth_cookie_response.ok:
193
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
194
-
195
- _logger.debug("Step 3 - Login")
196
-
197
- username_fieldname = self._country_settings["oauth_username_fieldname"]
198
- pasword_fieldname = self._country_settings["oauth_password_fieldname"]
199
-
200
- payload = {
201
- username_fieldname: self.username,
202
- pasword_fieldname: self.password,
203
- "rememberme": "true",
204
- }
205
-
206
- login_response = login_session.post(
207
- self._country_settings["oauth_url"], payload, allow_redirects=False
208
- )
209
- if not login_response.ok:
210
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
211
- redirect_url = login_response.headers[
212
- self._country_settings["oauth_redirect_header"]
213
- ]
214
-
215
- if self._identifier is not None:
216
- redirect_url += f"&dtv_identifier={self._identifier}"
217
- redirect_response = login_session.get(redirect_url, allow_redirects=False)
218
- success_url = redirect_response.headers[
219
- self._country_settings["oauth_redirect_header"]
220
- ]
221
- code_matches = re.findall(r"code=(.*)&", success_url)
222
-
223
- authorization_code = code_matches[0]
224
-
225
- new_payload = {
226
- "authorizationGrant": {
227
- "authorizationCode": authorization_code,
228
- "validityToken": authorization_validity_token,
229
- }
230
- }
231
- headers = {
232
- "content-type": "application/json",
233
- }
234
- post_result = login_session.post(
235
- auth_url, json.dumps(new_payload), headers=headers
236
- )
237
- self._auth.fill(post_result.json())
238
- self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
239
- except Exception:
240
- pass
241
-
242
170
  def _obtain_mqtt_token(self):
243
171
  _logger.debug("Obtain mqtt token...")
244
172
  mqtt_auth_url = self._config["authorizationService"]["URL"]
@@ -293,11 +293,17 @@ class LGHorizonRecordingListSeasonShow(LGHorizonBaseRecording):
293
293
  def __init__(self, recording_season_json):
294
294
  """Init the single recording."""
295
295
 
296
+ poster_url = None
297
+ if (
298
+ "poster" in recording_season_json
299
+ and "url" in recording_season_json["poster"]
300
+ ):
301
+ poster_url = recording_season_json["poster"]["url"]
296
302
  LGHorizonBaseRecording.__init__(
297
303
  self,
298
304
  recording_season_json["id"],
299
305
  recording_season_json["title"],
300
- recording_season_json["poster"]["url"],
306
+ poster_url,
301
307
  recording_season_json["channelId"],
302
308
  recording_season_json["type"],
303
309
  )
@@ -360,7 +366,6 @@ class LGHorizonMqttClient:
360
366
  )
361
367
  self.client_id = make_id()
362
368
  self._mqtt_client = mqtt.Client(
363
- mqtt.CallbackAPIVersion.VERSION1,
364
369
  client_id=self.client_id,
365
370
  transport="websockets",
366
371
  )
@@ -657,7 +662,7 @@ class LGHorizonBox:
657
662
  '{"id":"'
658
663
  + make_id(8)
659
664
  + '","type":"CPE.pushToTV","source":{"clientId":"'
660
- + self._mqtt_client.clientId
665
+ + self._mqtt_client.client_id
661
666
  + '","friendlyDeviceName":"Home Assistant"},'
662
667
  + '"status":{"sourceType":"linear","source":{"channelId":"'
663
668
  + channel.id
@@ -674,7 +679,7 @@ class LGHorizonBox:
674
679
  '{"id":"'
675
680
  + make_id(8)
676
681
  + '","type":"CPE.pushToTV","source":{"clientId":"'
677
- + self._mqtt_client.clientId
682
+ + self._mqtt_client.client_id
678
683
  + '","friendlyDeviceName":"Home Assistant"},'
679
684
  + '"status":{"sourceType":"nDVR","source":{"recordingId":"'
680
685
  + recording_id
@@ -686,23 +691,26 @@ class LGHorizonBox:
686
691
 
687
692
  def send_key_to_box(self, key: str) -> None:
688
693
  """Send emulated (remote) key press to settopbox."""
689
- payload = (
690
- '{"type":"CPE.KeyEvent","status":{"w3cKey":"'
691
- + key
692
- + '","eventType":"keyDownUp"}}'
693
- )
694
+ payload_dict = {
695
+ "type": "CPE.KeyEvent",
696
+ "runtimeType": "key",
697
+ "id": "ha",
698
+ "source": self.device_id.lower(),
699
+ "status": {"w3cKey": key, "eventType": "keyDownUp"},
700
+ }
701
+ payload = json.dumps(payload_dict)
694
702
  self._mqtt_client.publish_message(
695
703
  f"{self._auth.household_id}/{self.device_id}", payload
696
704
  )
697
705
 
698
- def _set_unknown_channel_info(self) -> None:
699
- """Set unknown channel info."""
700
- _logger.warning("Couldn't set channel. Channel info set to unknown...")
701
- self.playing_info.set_source_type(BOX_PLAY_STATE_CHANNEL)
702
- self.playing_info.set_channel(None)
703
- self.playing_info.set_title("No information available")
704
- self.playing_info.set_image(None)
705
- self.playing_info.set_paused(False)
706
+ # def _set_unknown_channel_info(self) -> None:
707
+ # """Set unknown channel info."""
708
+ # _logger.warning("Couldn't set channel. Channel info set to unknown...")
709
+ # self.playing_info.set_source_type(BOX_PLAY_STATE_CHANNEL)
710
+ # self.playing_info.set_channel(None)
711
+ # self.playing_info.set_title("No information available")
712
+ # self.playing_info.set_image(None)
713
+ # self.playing_info.set_paused(False)
706
714
 
707
715
  def _request_settop_box_state(self) -> None:
708
716
  """Send mqtt message to receive state from settop box."""
@@ -0,0 +1,270 @@
1
+ """LG Horizon API client."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, cast
5
+
6
+ from .lghorizon_device import LGHorizonDevice
7
+ from .lghorizon_models import LGHorizonChannel
8
+ from .lghorizon_models import LGHorizonAuth
9
+ from .lghorizon_models import LGHorizonCustomer
10
+ from .lghorizon_mqtt_client import LGHorizonMqttClient
11
+ from .lghorizon_models import LGHorizonServicesConfig
12
+ from .lghorizon_models import LGHorizonEntitlements
13
+ from .lghorizon_models import LGHorizonProfile
14
+ from .lghorizon_models import LGHorizonMessageType
15
+ from .lghorizon_message_factory import LGHorizonMessageFactory
16
+ from .lghorizon_models import LGHorizonStatusMessage, LGHorizonUIStatusMessage
17
+ from .lghorizon_models import LGHorizonRunningState
18
+ from .lghorizon_models import LGHorizonRecordingList, LGHorizonRecordingQuota
19
+ from .lghorizon_recording_factory import LGHorizonRecordingFactory
20
+ from .lghorizon_device_state_processor import LGHorizonDeviceStateProcessor
21
+
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class LGHorizonApi:
27
+ """LG Horizon API client."""
28
+
29
+ _mqtt_client: LGHorizonMqttClient
30
+ auth: LGHorizonAuth
31
+ _service_config: LGHorizonServicesConfig
32
+ _customer: LGHorizonCustomer
33
+ _channels: Dict[str, LGHorizonChannel]
34
+ _entitlements: LGHorizonEntitlements
35
+ _profile_id: str
36
+ _initialized: bool = False
37
+ _devices: Dict[str, LGHorizonDevice] = {}
38
+ _message_factory: LGHorizonMessageFactory = LGHorizonMessageFactory()
39
+ _device_state_processor: LGHorizonDeviceStateProcessor | None
40
+ _recording_factory: LGHorizonRecordingFactory = LGHorizonRecordingFactory()
41
+
42
+ def __init__(self, auth: LGHorizonAuth, profile_id: str = "") -> None:
43
+ """Initialize LG Horizon API client."""
44
+ self.auth = auth
45
+ self._profile_id = profile_id
46
+ self._channels = {}
47
+ self._device_state_processor = None
48
+
49
+ async def initialize(self) -> None:
50
+ """Initialize the API client."""
51
+ self._service_config = await self.auth.get_service_config()
52
+ self._customer = await self._get_customer_info()
53
+ if self._profile_id == "":
54
+ self._profile_id = list(self._customer.profiles.keys())[0]
55
+ await self._refresh_entitlements()
56
+ await self._refresh_channels()
57
+ self._mqtt_client = await self._create_mqtt_client()
58
+ await self._mqtt_client.connect()
59
+ await self._register_devices()
60
+ self._device_state_processor = LGHorizonDeviceStateProcessor(
61
+ self.auth, self._channels, self._customer, self._profile_id
62
+ )
63
+ self._initialized = True
64
+
65
+ async def get_devices(self) -> Dict[str, LGHorizonDevice]:
66
+ """Get devices."""
67
+ if not self._initialized:
68
+ raise RuntimeError("LGHorizonApi not initialized")
69
+
70
+ return self._devices
71
+
72
+ async def get_profiles(self) -> Dict[str, LGHorizonProfile]:
73
+ """Get profile IDs."""
74
+ if not self._initialized:
75
+ raise RuntimeError("LGHorizonApi not initialized")
76
+
77
+ return self._customer.profiles
78
+
79
+ async def get_profile_channels(
80
+ self, profile_id: str
81
+ ) -> Dict[str, LGHorizonChannel]:
82
+ """Returns channels to display baed on profile."""
83
+ # Attempt to retrieve the profile by the given profile_id
84
+ profile = self._customer.profiles.get(profile_id)
85
+
86
+ # If the specified profile is not found, and there are other profiles available,
87
+ # default to the first profile in the customer's list.
88
+ if not profile and self._customer.profiles:
89
+ _LOGGER.debug(
90
+ "Profile with ID '%s' not found. Defaulting to first available profile.",
91
+ profile_id,
92
+ )
93
+ profile = list(self._customer.profiles.values())[0]
94
+
95
+ # If a profile is found and it has favorite channels, filter the main channels list.
96
+ if profile and profile.favorite_channels:
97
+ _LOGGER.debug("Returning favorite channels for profile '%s'.", profile.name)
98
+ # Use a set for faster lookup of favorite channel IDs
99
+ profile_channel_ids = set(profile.favorite_channels)
100
+ return {
101
+ channel.id: channel
102
+ for channel in self._channels.values()
103
+ if channel.id in profile_channel_ids
104
+ }
105
+
106
+ # If no profile is found (even after defaulting) or the profile has no favorite channels,
107
+ # return all available channels.
108
+ _LOGGER.debug("No specific profile channels found, returning all channels.")
109
+ return self._channels
110
+
111
+ async def _register_devices(self) -> None:
112
+ """Register devices."""
113
+ _LOGGER.debug("Registering devices...")
114
+ self._devices = {}
115
+ channels = await self.get_profile_channels(self._profile_id)
116
+ for raw_box in self._customer.assigned_devices:
117
+ _LOGGER.debug("Creating box for device: %s", raw_box)
118
+ if self._device_state_processor is None:
119
+ self._device_state_processor = LGHorizonDeviceStateProcessor(
120
+ self.auth, self._channels, self._customer, self._profile_id
121
+ )
122
+ device = LGHorizonDevice(
123
+ raw_box,
124
+ self._mqtt_client,
125
+ self._device_state_processor,
126
+ self.auth,
127
+ channels,
128
+ )
129
+ self._devices[device.device_id] = device
130
+
131
+ async def disconnect(self) -> None:
132
+ """Disconnect the client."""
133
+ if self._mqtt_client:
134
+ await self._mqtt_client.disconnect()
135
+ self._initialized = False
136
+
137
+ async def _create_mqtt_client(self) -> LGHorizonMqttClient:
138
+ mqtt_client = await LGHorizonMqttClient.create(
139
+ self.auth,
140
+ self._on_mqtt_connected,
141
+ self._on_mqtt_message,
142
+ )
143
+ return mqtt_client
144
+
145
+ async def _on_mqtt_connected(self):
146
+ """MQTT connected callback."""
147
+ await self._mqtt_client.subscribe(self.auth.household_id)
148
+ # await self._mqtt_client.subscribe(self.auth.household_id + "/#")
149
+ await self._mqtt_client.subscribe(
150
+ self.auth.household_id + "/" + self._mqtt_client.client_id
151
+ )
152
+ await self._mqtt_client.subscribe(self.auth.household_id + "/+/status")
153
+ await self._mqtt_client.subscribe(
154
+ self.auth.household_id + "/+/networkRecordings"
155
+ )
156
+ await self._mqtt_client.subscribe(
157
+ self.auth.household_id + "/+/networkRecordings/capacity"
158
+ )
159
+ await self._mqtt_client.subscribe(self.auth.household_id + "/+/localRecordings")
160
+ await self._mqtt_client.subscribe(
161
+ self.auth.household_id + "/+/localRecordings/capacity"
162
+ )
163
+ await self._mqtt_client.subscribe(self.auth.household_id + "/watchlistService")
164
+ await self._mqtt_client.subscribe(self.auth.household_id + "/purchaseService")
165
+ await self._mqtt_client.subscribe(
166
+ self.auth.household_id + "/personalizationService"
167
+ )
168
+ await self._mqtt_client.subscribe(self.auth.household_id + "/recordingStatus")
169
+ await self._mqtt_client.subscribe(
170
+ self.auth.household_id + "/recordingStatus/lastUserAction"
171
+ )
172
+
173
+ async def _on_mqtt_message(self, mqtt_message: dict, mqtt_topic: str):
174
+ """MQTT message callback."""
175
+ message = await self._message_factory.create_message(mqtt_topic, mqtt_message)
176
+ match message.message_type:
177
+ case LGHorizonMessageType.STATUS:
178
+ message.__class__ = LGHorizonStatusMessage
179
+ status_message = cast(LGHorizonStatusMessage, message)
180
+ device = self._devices[status_message.source]
181
+ await device.handle_status_message(status_message)
182
+ case LGHorizonMessageType.UI_STATUS:
183
+ message.__class__ = LGHorizonUIStatusMessage
184
+ ui_status_message = cast(LGHorizonUIStatusMessage, message)
185
+ device = self._devices[ui_status_message.source]
186
+ if (
187
+ not device.device_state.state
188
+ == LGHorizonRunningState.ONLINE_RUNNING
189
+ ):
190
+ return
191
+ await device.handle_ui_status_message(ui_status_message)
192
+
193
+ async def _get_customer_info(self) -> LGHorizonCustomer:
194
+ service_url = await self._service_config.get_service_url(
195
+ "personalizationService"
196
+ )
197
+ result = await self.auth.request(
198
+ service_url,
199
+ f"/v1/customer/{self.auth.household_id}?with=profiles%2Cdevices",
200
+ )
201
+ return LGHorizonCustomer(result)
202
+
203
+ async def _refresh_entitlements(self) -> Any:
204
+ """Retrieve entitlements."""
205
+ _LOGGER.debug("Retrieving entitlements...")
206
+ service_url = await self._service_config.get_service_url("purchaseService")
207
+ result = await self.auth.request(
208
+ service_url,
209
+ f"/v2/customers/{self.auth.household_id}/entitlements?enableDaypass=true",
210
+ )
211
+ self._entitlements = LGHorizonEntitlements(result)
212
+
213
+ async def _refresh_channels(self):
214
+ """Retrieve channels."""
215
+ _LOGGER.debug("Retrieving channels...")
216
+ service_url = await self._service_config.get_service_url("linearService")
217
+ lang = await self._customer.get_profile_lang(self._profile_id)
218
+ channels_json = await self.auth.request(
219
+ service_url,
220
+ f"/v2/channels?cityId={self._customer.city_id}&language={lang}&productClass=Orion-DASH",
221
+ )
222
+ for channel_json in channels_json:
223
+ channel = LGHorizonChannel(channel_json)
224
+ common_entitlements = list(
225
+ set(self._entitlements.entitlement_ids) & set(channel.linear_products)
226
+ )
227
+
228
+ if len(common_entitlements) == 0:
229
+ continue
230
+
231
+ self._channels[channel.id] = channel
232
+
233
+ async def get_all_recordings(self) -> LGHorizonRecordingList:
234
+ """Retrieve all recordings."""
235
+ _LOGGER.debug("Retrieving recordings...")
236
+ service_url = await self._service_config.get_service_url("recordingService")
237
+ lang = await self._customer.get_profile_lang(self._profile_id)
238
+ recordings_json = await self.auth.request(
239
+ service_url,
240
+ f"/customers/{self.auth.household_id}/recordings?isAdult=false&offset=0&limit=100&sort=time&sortOrder=desc&profileId={self._profile_id}&language={lang}",
241
+ )
242
+ recordings = await self._recording_factory.create_recordings(recordings_json)
243
+ return recordings
244
+
245
+ async def get_show_recordings(
246
+ self, show_id: str, channel_id: str
247
+ ) -> LGHorizonRecordingList:
248
+ """Retrieve all recordings."""
249
+ _LOGGER.debug("Retrieving recordings fro show...")
250
+ service_url = await self._service_config.get_service_url("recordingService")
251
+ lang = await self._customer.get_profile_lang(self._profile_id)
252
+ episodes_json = await self.auth.request(
253
+ service_url,
254
+ f"/customers/8436830_nl/episodes/shows/{show_id}?source=recording&isAdult=false&offset=0&limit=100&profileId={self._profile_id}&language={lang}&channelId={channel_id}&sort=time&sortOrder=asc",
255
+ )
256
+ recordings = await self._recording_factory.create_episodes(episodes_json)
257
+ return recordings
258
+
259
+ async def get_recording_quota(self) -> LGHorizonRecordingQuota:
260
+ """Refresh recording quota."""
261
+ _LOGGER.debug("Refreshing recording quota...")
262
+ service_url = await self._service_config.get_service_url("recordingService")
263
+ quota_json = await self.auth.request(
264
+ service_url,
265
+ f"/customers/{self.auth.household_id}/quota",
266
+ )
267
+ return LGHorizonRecordingQuota(quota_json)
268
+
269
+
270
+ __all__ = ["LGHorizonApi", "LGHorizonAuth"]