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.
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.github/workflows/build-on-pr.yml +1 -1
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.github/workflows/publish-to-pypi.yml +3 -3
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.gitignore +1 -0
- lghorizon-0.9.0b0/.vscode/launch.json +15 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/PKG-INFO +15 -3
- lghorizon-0.9.0b0/lghorizon/__init__.py +6 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/const.py +20 -4
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/helpers.py +1 -1
- {lghorizon-0.8.0b2/lghorizon → lghorizon-0.9.0b0/lghorizon/legacy}/lghorizon_api.py +1 -73
- {lghorizon-0.8.0b2/lghorizon → lghorizon-0.9.0b0/lghorizon/legacy}/models.py +25 -17
- lghorizon-0.9.0b0/lghorizon/lghorizon_api.py +270 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_device.py +336 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_device_state_processor.py +301 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_message_factory.py +39 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_models.py +1331 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_mqtt_client.py +123 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_recording_factory.py +41 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/PKG-INFO +15 -3
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/SOURCES.txt +11 -3
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/requires.txt +1 -1
- lghorizon-0.9.0b0/main.py +94 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/secrets_stub.json +2 -2
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/setup.py +1 -1
- lghorizon-0.8.0b2/lghorizon/__init__.py +0 -41
- lghorizon-0.8.0b2/test.py +0 -84
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.coverage +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/.flake8 +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/LICENSE +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/README.md +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/instructions.txt +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/exceptions.py +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon/py.typed +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/dependency_links.txt +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/not-zip-safe +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lghorizon.egg-info/top_level.txt +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/lib64 +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/pyvenv.cfg +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/renovate.json +0 -0
- {lghorizon-0.8.0b2 → lghorizon-0.9.0b0}/setup.cfg +0 -0
|
@@ -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.
|
|
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 }}
|
|
@@ -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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lghorizon
|
|
3
|
-
Version: 0.
|
|
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
|
|
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
|
|
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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"]
|