lghorizon 0.9.0b0__tar.gz → 0.9.0.dev4__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.9.0b0 → lghorizon-0.9.0.dev4}/.gitignore +0 -1
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/PKG-INFO +1 -1
- lghorizon-0.9.0.dev4/lghorizon/__init__.py +41 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon/const.py +4 -11
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon/helpers.py +1 -1
- {lghorizon-0.9.0b0/lghorizon/legacy → lghorizon-0.9.0.dev4/lghorizon}/lghorizon_api.py +70 -0
- {lghorizon-0.9.0b0/lghorizon/legacy → lghorizon-0.9.0.dev4/lghorizon}/models.py +8 -8
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon.egg-info/PKG-INFO +1 -1
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon.egg-info/SOURCES.txt +3 -11
- lghorizon-0.9.0.dev4/test.py +84 -0
- lghorizon-0.9.0b0/.vscode/launch.json +0 -15
- lghorizon-0.9.0b0/lghorizon/__init__.py +0 -6
- lghorizon-0.9.0b0/lghorizon/lghorizon_api.py +0 -270
- lghorizon-0.9.0b0/lghorizon/lghorizon_device.py +0 -336
- lghorizon-0.9.0b0/lghorizon/lghorizon_device_state_processor.py +0 -301
- lghorizon-0.9.0b0/lghorizon/lghorizon_message_factory.py +0 -39
- lghorizon-0.9.0b0/lghorizon/lghorizon_models.py +0 -1331
- lghorizon-0.9.0b0/lghorizon/lghorizon_mqtt_client.py +0 -123
- lghorizon-0.9.0b0/lghorizon/lghorizon_recording_factory.py +0 -41
- lghorizon-0.9.0b0/main.py +0 -94
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/.coverage +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/.flake8 +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/.github/workflows/build-on-pr.yml +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/.github/workflows/publish-to-pypi.yml +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/LICENSE +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/README.md +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/instructions.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon/exceptions.py +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon/py.typed +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon.egg-info/dependency_links.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon.egg-info/not-zip-safe +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon.egg-info/requires.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lghorizon.egg-info/top_level.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/lib64 +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/pyvenv.cfg +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/renovate.json +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/secrets_stub.json +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/setup.cfg +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev4}/setup.py +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"LGHorizonApi",
|
|
27
|
+
"LGHorizonBox",
|
|
28
|
+
"LGHorizonRecordingListSeasonShow",
|
|
29
|
+
"LGHorizonRecordingSingle",
|
|
30
|
+
"LGHorizonRecordingShow",
|
|
31
|
+
"LGHorizonRecordingEpisode",
|
|
32
|
+
"LGHorizonCustomer",
|
|
33
|
+
"LGHorizonApiUnauthorizedError",
|
|
34
|
+
"LGHorizonApiConnectionError",
|
|
35
|
+
"LGHorizonApiLockedError",
|
|
36
|
+
"ONLINE_RUNNING",
|
|
37
|
+
"ONLINE_STANDBY",
|
|
38
|
+
"RECORDING_TYPE_SHOW",
|
|
39
|
+
"RECORDING_TYPE_SEASON",
|
|
40
|
+
"RECORDING_TYPE_SINGLE",
|
|
41
|
+
] # noqa
|
|
@@ -38,18 +38,11 @@ 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
|
-
|
|
48
41
|
COUNTRY_SETTINGS = {
|
|
49
42
|
"nl": {
|
|
50
43
|
"api_url": "https://spark-prod-nl.gnp.cloud.ziggogo.tv",
|
|
51
44
|
"mqtt_url": "obomsg.prod.nl.horizon.tv",
|
|
52
|
-
"
|
|
45
|
+
"use_oauth": False,
|
|
53
46
|
"channels": [
|
|
54
47
|
{
|
|
55
48
|
"channelId": "NL_000073_019506",
|
|
@@ -113,7 +106,7 @@ COUNTRY_SETTINGS = {
|
|
|
113
106
|
},
|
|
114
107
|
"be-nl-preprod": {
|
|
115
108
|
"api_url": "https://spark-preprod-be.gnp.cloud.telenet.tv",
|
|
116
|
-
"
|
|
109
|
+
"use_oauth": True,
|
|
117
110
|
"oauth_username_fieldname": "j_username",
|
|
118
111
|
"oauth_password_fieldname": "j_password",
|
|
119
112
|
"oauth_add_accept_header": False,
|
|
@@ -138,13 +131,13 @@ COUNTRY_SETTINGS = {
|
|
|
138
131
|
},
|
|
139
132
|
"ie": {
|
|
140
133
|
"api_url": "https://spark-prod-ie.gnp.cloud.virginmediatv.ie",
|
|
141
|
-
"
|
|
134
|
+
"use_oauth": False,
|
|
142
135
|
"channels": [],
|
|
143
136
|
"language": "en",
|
|
144
137
|
},
|
|
145
138
|
"pl": {
|
|
146
139
|
"api_url": "https://spark-prod-pl.gnp.cloud.upctv.pl",
|
|
147
|
-
"
|
|
140
|
+
"use_oauth": False,
|
|
148
141
|
"channels": [],
|
|
149
142
|
"language": "pl",
|
|
150
143
|
"platform_types": {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import random
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
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))
|
|
@@ -167,6 +167,76 @@ class LGHorizonApi:
|
|
|
167
167
|
"""Set the refresh callback."""
|
|
168
168
|
self._refresh_callback = refresh_callback
|
|
169
169
|
|
|
170
|
+
def _authorize_telenet(self):
|
|
171
|
+
"""Authorize telenet users."""
|
|
172
|
+
try:
|
|
173
|
+
login_session = Session()
|
|
174
|
+
# Step 1 - Get Authorization data
|
|
175
|
+
_logger.debug("Step 1 - Get Authorization data")
|
|
176
|
+
auth_url = (
|
|
177
|
+
f"{self._country_settings['api_url']}/auth-service/v1/sso/authorization"
|
|
178
|
+
)
|
|
179
|
+
auth_response = login_session.get(auth_url)
|
|
180
|
+
if not auth_response.ok:
|
|
181
|
+
raise LGHorizonApiConnectionError("Can't connect to authorization URL")
|
|
182
|
+
auth_response_json = auth_response.json()
|
|
183
|
+
authorization_uri = auth_response_json["authorizationUri"]
|
|
184
|
+
authorization_validity_token = auth_response_json["validityToken"]
|
|
185
|
+
|
|
186
|
+
# Step 2 - Get Authorization cookie
|
|
187
|
+
_logger.debug("Step 2 - Get Authorization cookie")
|
|
188
|
+
|
|
189
|
+
auth_cookie_response = login_session.get(authorization_uri)
|
|
190
|
+
if not auth_cookie_response.ok:
|
|
191
|
+
raise LGHorizonApiConnectionError("Can't connect to authorization URL")
|
|
192
|
+
|
|
193
|
+
_logger.debug("Step 3 - Login")
|
|
194
|
+
|
|
195
|
+
username_fieldname = self._country_settings["oauth_username_fieldname"]
|
|
196
|
+
pasword_fieldname = self._country_settings["oauth_password_fieldname"]
|
|
197
|
+
|
|
198
|
+
payload = {
|
|
199
|
+
username_fieldname: self.username,
|
|
200
|
+
pasword_fieldname: self.password,
|
|
201
|
+
"rememberme": "true",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
login_response = login_session.post(
|
|
205
|
+
self._country_settings["oauth_url"], payload, allow_redirects=False
|
|
206
|
+
)
|
|
207
|
+
if not login_response.ok:
|
|
208
|
+
raise LGHorizonApiConnectionError("Can't connect to authorization URL")
|
|
209
|
+
redirect_url = login_response.headers[
|
|
210
|
+
self._country_settings["oauth_redirect_header"]
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
if self._identifier is not None:
|
|
214
|
+
redirect_url += f"&dtv_identifier={self._identifier}"
|
|
215
|
+
redirect_response = login_session.get(redirect_url, allow_redirects=False)
|
|
216
|
+
success_url = redirect_response.headers[
|
|
217
|
+
self._country_settings["oauth_redirect_header"]
|
|
218
|
+
]
|
|
219
|
+
code_matches = re.findall(r"code=(.*)&", success_url)
|
|
220
|
+
|
|
221
|
+
authorization_code = code_matches[0]
|
|
222
|
+
|
|
223
|
+
new_payload = {
|
|
224
|
+
"authorizationGrant": {
|
|
225
|
+
"authorizationCode": authorization_code,
|
|
226
|
+
"validityToken": authorization_validity_token,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
headers = {
|
|
230
|
+
"content-type": "application/json",
|
|
231
|
+
}
|
|
232
|
+
post_result = login_session.post(
|
|
233
|
+
auth_url, json.dumps(new_payload), headers=headers
|
|
234
|
+
)
|
|
235
|
+
self._auth.fill(post_result.json())
|
|
236
|
+
self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
|
|
170
240
|
def _obtain_mqtt_token(self):
|
|
171
241
|
_logger.debug("Obtain mqtt token...")
|
|
172
242
|
mqtt_auth_url = self._config["authorizationService"]["URL"]
|
|
@@ -703,14 +703,14 @@ class LGHorizonBox:
|
|
|
703
703
|
f"{self._auth.household_id}/{self.device_id}", payload
|
|
704
704
|
)
|
|
705
705
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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)
|
|
714
714
|
|
|
715
715
|
def _request_settop_box_state(self) -> None:
|
|
716
716
|
"""Send mqtt message to receive state from settop box."""
|
|
@@ -5,31 +5,23 @@ LICENSE
|
|
|
5
5
|
README.md
|
|
6
6
|
instructions.txt
|
|
7
7
|
lib64
|
|
8
|
-
main.py
|
|
9
8
|
pyvenv.cfg
|
|
10
9
|
renovate.json
|
|
11
10
|
secrets_stub.json
|
|
12
11
|
setup.py
|
|
12
|
+
test.py
|
|
13
13
|
.github/workflows/build-on-pr.yml
|
|
14
14
|
.github/workflows/publish-to-pypi.yml
|
|
15
|
-
.vscode/launch.json
|
|
16
15
|
lghorizon/__init__.py
|
|
17
16
|
lghorizon/const.py
|
|
18
17
|
lghorizon/exceptions.py
|
|
19
18
|
lghorizon/helpers.py
|
|
20
19
|
lghorizon/lghorizon_api.py
|
|
21
|
-
lghorizon/
|
|
22
|
-
lghorizon/lghorizon_device_state_processor.py
|
|
23
|
-
lghorizon/lghorizon_message_factory.py
|
|
24
|
-
lghorizon/lghorizon_models.py
|
|
25
|
-
lghorizon/lghorizon_mqtt_client.py
|
|
26
|
-
lghorizon/lghorizon_recording_factory.py
|
|
20
|
+
lghorizon/models.py
|
|
27
21
|
lghorizon/py.typed
|
|
28
22
|
lghorizon.egg-info/PKG-INFO
|
|
29
23
|
lghorizon.egg-info/SOURCES.txt
|
|
30
24
|
lghorizon.egg-info/dependency_links.txt
|
|
31
25
|
lghorizon.egg-info/not-zip-safe
|
|
32
26
|
lghorizon.egg-info/requires.txt
|
|
33
|
-
lghorizon.egg-info/top_level.txt
|
|
34
|
-
lghorizon/legacy/lghorizon_api.py
|
|
35
|
-
lghorizon/legacy/models.py
|
|
27
|
+
lghorizon.egg-info/top_level.txt
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
""" "Test the component."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from lghorizon import LGHorizonApi
|
|
7
|
+
|
|
8
|
+
api: LGHorizonApi
|
|
9
|
+
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.DEBUG,
|
|
12
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_Logger = logging.getLogger()
|
|
16
|
+
|
|
17
|
+
file_handler = logging.FileHandler("logfile.log", mode="w")
|
|
18
|
+
file_handler.setLevel(logging.DEBUG)
|
|
19
|
+
_Logger.addHandler(file_handler)
|
|
20
|
+
|
|
21
|
+
console_handler = logging.StreamHandler()
|
|
22
|
+
console_handler.setLevel(logging.DEBUG)
|
|
23
|
+
_Logger.addHandler(console_handler)
|
|
24
|
+
|
|
25
|
+
secrets: dict[str, str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_secrets(file_path):
|
|
29
|
+
"""Read secrets from file."""
|
|
30
|
+
try:
|
|
31
|
+
with open(file_path, "r", encoding="UTF-8") as file:
|
|
32
|
+
return json.load(file)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
print(f"Error: Secrets file not found at {file_path}")
|
|
35
|
+
return {}
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
print(f"Error: Unable to decode JSON in {file_path}")
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def event_loop():
|
|
42
|
+
"""Default event loop."""
|
|
43
|
+
while True:
|
|
44
|
+
time.sleep(1) # Simulate some work
|
|
45
|
+
|
|
46
|
+
# Check for a breaking condition
|
|
47
|
+
if break_condition():
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def break_condition():
|
|
52
|
+
"""Break event loop on conditions."""
|
|
53
|
+
# Implement your breaking condition logic here
|
|
54
|
+
return False # Change this condition based on your requirements
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
try:
|
|
59
|
+
secrets = read_secrets("secrets.json")
|
|
60
|
+
|
|
61
|
+
refresh_token: str = None
|
|
62
|
+
if "refresh_token" in secrets:
|
|
63
|
+
refresh_token = secrets["refresh_token"]
|
|
64
|
+
|
|
65
|
+
profile_id: str = None
|
|
66
|
+
if "profile_id" in secrets:
|
|
67
|
+
profile_id = secrets["profile_id"]
|
|
68
|
+
|
|
69
|
+
api = LGHorizonApi(
|
|
70
|
+
secrets["username"],
|
|
71
|
+
secrets["password"],
|
|
72
|
+
secrets["country"],
|
|
73
|
+
# identifier="DTV3907048",
|
|
74
|
+
refresh_token=refresh_token,
|
|
75
|
+
profile_id=profile_id,
|
|
76
|
+
)
|
|
77
|
+
api.connect()
|
|
78
|
+
event_loop()
|
|
79
|
+
except KeyboardInterrupt:
|
|
80
|
+
print("\nScript interrupted by user.")
|
|
81
|
+
finally:
|
|
82
|
+
print("Script is exiting.")
|
|
83
|
+
if api:
|
|
84
|
+
api.disconnect()
|
|
@@ -1,15 +0,0 @@
|
|
|
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,270 +0,0 @@
|
|
|
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"]
|