lghorizon 0.9.0b0__tar.gz → 0.9.0.dev1__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.dev1}/PKG-INFO +1 -1
- lghorizon-0.9.0.dev1/lghorizon/__init__.py +36 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_device_state_processor.py → lghorizon-0.9.0.dev1/lghorizon/device_state_processor.py +9 -9
- lghorizon-0.9.0b0/lghorizon/lghorizon_api.py → lghorizon-0.9.0.dev1/lghorizon/lghorizonapi.py +16 -16
- lghorizon-0.9.0b0/lghorizon/lghorizon_message_factory.py → lghorizon-0.9.0.dev1/lghorizon/message_factory.py +1 -1
- lghorizon-0.9.0.dev1/lghorizon/models/__init__.py +12 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_auth.py +227 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_channel.py +53 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_config.py +65 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_customer.py +55 -0
- {lghorizon-0.9.0b0/lghorizon → lghorizon-0.9.0.dev1/lghorizon/models}/lghorizon_device.py +31 -25
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_device_state.py +180 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_entitlements.py +21 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_events.py +119 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_message.py +113 -0
- {lghorizon-0.9.0b0/lghorizon → lghorizon-0.9.0.dev1/lghorizon/models}/lghorizon_mqtt_client.py +7 -4
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_profile.py +46 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_recordings.py +243 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_sources.py +127 -0
- lghorizon-0.9.0.dev1/lghorizon/models/lghorizon_ui_status.py +127 -0
- lghorizon-0.9.0b0/lghorizon/lghorizon_recording_factory.py → lghorizon-0.9.0.dev1/lghorizon/recording_factory.py +1 -1
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon.egg-info/PKG-INFO +1 -1
- lghorizon-0.9.0.dev1/lghorizon.egg-info/SOURCES.txt +47 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/main.py +2 -2
- lghorizon-0.9.0b0/lghorizon/__init__.py +0 -6
- lghorizon-0.9.0b0/lghorizon/lghorizon_models.py +0 -1331
- lghorizon-0.9.0b0/lghorizon.egg-info/SOURCES.txt +0 -35
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/.coverage +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/.flake8 +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/.github/workflows/build-on-pr.yml +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/.github/workflows/publish-to-pypi.yml +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/.gitignore +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/.vscode/launch.json +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/LICENSE +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/README.md +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/instructions.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon/const.py +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon/helpers.py +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon/legacy/lghorizon_api.py +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon/legacy/models.py +0 -0
- {lghorizon-0.9.0b0/lghorizon → lghorizon-0.9.0.dev1/lghorizon/models}/exceptions.py +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon/py.typed +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon.egg-info/dependency_links.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon.egg-info/not-zip-safe +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon.egg-info/requires.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lghorizon.egg-info/top_level.txt +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/lib64 +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/pyvenv.cfg +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/renovate.json +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/secrets_stub.json +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/setup.cfg +0 -0
- {lghorizon-0.9.0b0 → lghorizon-0.9.0.dev1}/setup.py +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Python client for LG Horizon."""
|
|
2
|
+
|
|
3
|
+
from .lghorizonapi import LGHorizonApi
|
|
4
|
+
from .models.lghorizon_auth import (
|
|
5
|
+
LGHorizonAuth,
|
|
6
|
+
)
|
|
7
|
+
from .models.exceptions import (
|
|
8
|
+
LGHorizonApiUnauthorizedError,
|
|
9
|
+
LGHorizonApiConnectionError,
|
|
10
|
+
LGHorizonApiLockedError,
|
|
11
|
+
)
|
|
12
|
+
from .const import (
|
|
13
|
+
ONLINE_RUNNING,
|
|
14
|
+
ONLINE_STANDBY,
|
|
15
|
+
RECORDING_TYPE_SHOW,
|
|
16
|
+
RECORDING_TYPE_SEASON,
|
|
17
|
+
RECORDING_TYPE_SINGLE,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"LGHorizonApi",
|
|
22
|
+
"LGHorizonBox",
|
|
23
|
+
"LGHorizonRecordingListSeasonShow",
|
|
24
|
+
"LGHorizonRecordingSingle",
|
|
25
|
+
"LGHorizonRecordingShow",
|
|
26
|
+
"LGHorizonRecordingEpisode",
|
|
27
|
+
"LGHorizonCustomer",
|
|
28
|
+
"LGHorizonApiUnauthorizedError",
|
|
29
|
+
"LGHorizonApiConnectionError",
|
|
30
|
+
"LGHorizonApiLockedError",
|
|
31
|
+
"ONLINE_RUNNING",
|
|
32
|
+
"ONLINE_STANDBY",
|
|
33
|
+
"RECORDING_TYPE_SHOW",
|
|
34
|
+
"RECORDING_TYPE_SEASON",
|
|
35
|
+
"RECORDING_TYPE_SINGLE",
|
|
36
|
+
] # noqa
|
|
@@ -6,9 +6,9 @@ import urllib.parse
|
|
|
6
6
|
|
|
7
7
|
from typing import cast, Dict, Optional
|
|
8
8
|
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
9
|
+
from .models.lghorizon_device_state import LGHorizonDeviceState, LGHorizonRunningState
|
|
10
|
+
from .models.lghorizon_message import LGHorizonStatusMessage, LGHorizonUIStatusMessage
|
|
11
|
+
from .models.lghorizon_sources import (
|
|
12
12
|
LGHorizonSourceType,
|
|
13
13
|
LGHorizonLinearSource,
|
|
14
14
|
LGHorizonVODSource,
|
|
@@ -16,20 +16,20 @@ from .lghorizon_models import (
|
|
|
16
16
|
LGHorizonNDVRSource,
|
|
17
17
|
LGHorizonReviewBufferSource,
|
|
18
18
|
)
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
19
|
+
from .models.lghorizon_auth import LGHorizonAuth
|
|
20
|
+
from .models.lghorizon_events import (
|
|
21
21
|
LGHorizonReplayEvent,
|
|
22
22
|
LGHorizonVOD,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
from .
|
|
26
|
-
from .
|
|
27
|
-
from .
|
|
25
|
+
from .models.lghorizon_recordings import LGHorizonRecordingSingle
|
|
26
|
+
from .models.lghorizon_channel import LGHorizonChannel
|
|
27
|
+
from .models.lghorizon_ui_status import (
|
|
28
28
|
LGHorizonUIStateType,
|
|
29
29
|
LGHorizonAppsState,
|
|
30
30
|
LGHorizonPlayerState,
|
|
31
31
|
)
|
|
32
|
-
from .
|
|
32
|
+
from .models.lghorizon_customer import LGHorizonCustomer
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class LGHorizonDeviceStateProcessor:
|
lghorizon-0.9.0b0/lghorizon/lghorizon_api.py → lghorizon-0.9.0.dev1/lghorizon/lghorizonapi.py
RENAMED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any, Dict, cast
|
|
5
5
|
|
|
6
|
-
from .lghorizon_device import LGHorizonDevice
|
|
7
|
-
from .
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .lghorizon_mqtt_client import LGHorizonMqttClient
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
from .
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
6
|
+
from .models.lghorizon_device import LGHorizonDevice
|
|
7
|
+
from .models.lghorizon_channel import LGHorizonChannel
|
|
8
|
+
from .models.lghorizon_auth import LGHorizonAuth
|
|
9
|
+
from .models.lghorizon_customer import LGHorizonCustomer
|
|
10
|
+
from .models.lghorizon_mqtt_client import LGHorizonMqttClient
|
|
11
|
+
from .models.lghorizon_config import LGHorizonServicesConfig
|
|
12
|
+
from .models.lghorizon_entitlements import LGHorizonEntitlements
|
|
13
|
+
from .models.lghorizon_profile import LGHorizonProfile
|
|
14
|
+
from .models.lghorizon_message import LGHorizonMessageType
|
|
15
|
+
from .message_factory import LGHorizonMessageFactory
|
|
16
|
+
from .models.lghorizon_message import LGHorizonStatusMessage, LGHorizonUIStatusMessage
|
|
17
|
+
from .models.lghorizon_device_state import LGHorizonRunningState
|
|
18
|
+
from .models.lghorizon_recordings import LGHorizonRecordingList, LGHorizonRecordingQuota
|
|
19
|
+
from .recording_factory import LGHorizonRecordingFactory
|
|
20
|
+
from .device_state_processor import LGHorizonDeviceStateProcessor
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -190,7 +190,7 @@ class LGHorizonApi:
|
|
|
190
190
|
return
|
|
191
191
|
await device.handle_ui_status_message(ui_status_message)
|
|
192
192
|
|
|
193
|
-
async def _get_customer_info(self) ->
|
|
193
|
+
async def _get_customer_info(self) -> Any:
|
|
194
194
|
service_url = await self._service_config.get_service_url(
|
|
195
195
|
"personalizationService"
|
|
196
196
|
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Models for LG Horizon."""
|
|
2
|
+
|
|
3
|
+
from .lghorizon_auth import LGHorizonAuth
|
|
4
|
+
from .lghorizon_config import LGHorizonServicesConfig
|
|
5
|
+
from .lghorizon_message import LGHorizonMessage, LGHorizonStatusMessage
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"LGHorizonAuth",
|
|
9
|
+
"LGHorizonServicesConfig",
|
|
10
|
+
"LGHorizonMessage",
|
|
11
|
+
"LGHorizonStatusMessage",
|
|
12
|
+
]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""LG Horizon Auth Model."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import backoff
|
|
9
|
+
from aiohttp import ClientResponseError, ClientSession
|
|
10
|
+
|
|
11
|
+
from ..const import COUNTRY_SETTINGS
|
|
12
|
+
from .exceptions import LGHorizonApiConnectionError, LGHorizonApiUnauthorizedError
|
|
13
|
+
from .lghorizon_config import LGHorizonServicesConfig
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LGHorizonAuth:
|
|
19
|
+
"""Class to make authenticated requests."""
|
|
20
|
+
|
|
21
|
+
_websession: ClientSession
|
|
22
|
+
_refresh_token: str
|
|
23
|
+
_access_token: Optional[str]
|
|
24
|
+
_username: str
|
|
25
|
+
_password: str
|
|
26
|
+
_household_id: str
|
|
27
|
+
_token_expiry: Optional[int]
|
|
28
|
+
_country_code: str
|
|
29
|
+
_host: str
|
|
30
|
+
_use_refresh_token: bool
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
websession: ClientSession,
|
|
35
|
+
country_code: str,
|
|
36
|
+
refresh_token: str = "",
|
|
37
|
+
username: str = "",
|
|
38
|
+
password: str = "",
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize the auth with refresh token."""
|
|
41
|
+
self._websession = websession
|
|
42
|
+
self._refresh_token = refresh_token
|
|
43
|
+
self._access_token = None
|
|
44
|
+
self._username = username
|
|
45
|
+
self._password = password
|
|
46
|
+
self._household_id = ""
|
|
47
|
+
self._token_expiry = None
|
|
48
|
+
self._country_code = country_code
|
|
49
|
+
self._host = COUNTRY_SETTINGS[country_code]["api_url"]
|
|
50
|
+
self._use_refresh_token = COUNTRY_SETTINGS[country_code]["use_refreshtoken"]
|
|
51
|
+
self._service_config = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def websession(self) -> ClientSession:
|
|
55
|
+
"""Return the aiohttp client session."""
|
|
56
|
+
return self._websession
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def refresh_token(self) -> str:
|
|
60
|
+
"""Return the refresh token."""
|
|
61
|
+
return self._refresh_token
|
|
62
|
+
|
|
63
|
+
@refresh_token.setter
|
|
64
|
+
def refresh_token(self, value: str) -> None:
|
|
65
|
+
"""Set the refresh token."""
|
|
66
|
+
self._refresh_token = value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def access_token(self) -> Optional[str]:
|
|
70
|
+
"""Return the access token."""
|
|
71
|
+
return self._access_token
|
|
72
|
+
|
|
73
|
+
@access_token.setter
|
|
74
|
+
def access_token(self, value: Optional[str]) -> None:
|
|
75
|
+
"""Set the access token."""
|
|
76
|
+
self._access_token = value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def username(self) -> str:
|
|
80
|
+
"""Return the username."""
|
|
81
|
+
return self._username
|
|
82
|
+
|
|
83
|
+
@username.setter
|
|
84
|
+
def username(self, value: str) -> None:
|
|
85
|
+
"""Set the username."""
|
|
86
|
+
self._username = value
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def password(self) -> str:
|
|
90
|
+
"""Return the password."""
|
|
91
|
+
return self._password
|
|
92
|
+
|
|
93
|
+
@password.setter
|
|
94
|
+
def password(self, value: str) -> None:
|
|
95
|
+
"""Set the password."""
|
|
96
|
+
self._password = value
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def household_id(self) -> str:
|
|
100
|
+
"""Return the household ID."""
|
|
101
|
+
return self._household_id
|
|
102
|
+
|
|
103
|
+
@household_id.setter
|
|
104
|
+
def household_id(self, value: str) -> None:
|
|
105
|
+
"""Set the household ID."""
|
|
106
|
+
self._household_id = value
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def token_expiry(self) -> Optional[int]:
|
|
110
|
+
"""Return the token expiry timestamp."""
|
|
111
|
+
return self._token_expiry
|
|
112
|
+
|
|
113
|
+
@token_expiry.setter
|
|
114
|
+
def token_expiry(self, value: Optional[int]) -> None:
|
|
115
|
+
"""Set the token expiry timestamp."""
|
|
116
|
+
self._token_expiry = value
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def country_code(self) -> str:
|
|
120
|
+
"""Return the country code."""
|
|
121
|
+
return self._country_code
|
|
122
|
+
|
|
123
|
+
async def is_token_expiring(self) -> bool:
|
|
124
|
+
"""Check if the token is expiring within one day."""
|
|
125
|
+
if not self.access_token or not self.token_expiry:
|
|
126
|
+
return True
|
|
127
|
+
current_unix_time = int(time.time())
|
|
128
|
+
return current_unix_time >= (self.token_expiry - 86400)
|
|
129
|
+
|
|
130
|
+
async def fetch_access_token(self) -> None:
|
|
131
|
+
"""Fetch the access token."""
|
|
132
|
+
_LOGGER.debug("Fetching access token")
|
|
133
|
+
headers = dict()
|
|
134
|
+
headers["content-type"] = "application/json"
|
|
135
|
+
headers["charset"] = "utf-8"
|
|
136
|
+
|
|
137
|
+
if not self._use_refresh_token and self.access_token is None:
|
|
138
|
+
payload = {"password": self.password, "username": self.username}
|
|
139
|
+
headers["x-device-code"] = "web"
|
|
140
|
+
auth_url_path = "/auth-service/v1/authorization"
|
|
141
|
+
else:
|
|
142
|
+
payload = {"refreshToken": self.refresh_token}
|
|
143
|
+
auth_url_path = "/auth-service/v1/authorization/refresh"
|
|
144
|
+
try: # Use properties and backing fields
|
|
145
|
+
auth_response = await self.websession.post(
|
|
146
|
+
f"{self._host}{auth_url_path}",
|
|
147
|
+
json=payload,
|
|
148
|
+
headers=headers,
|
|
149
|
+
)
|
|
150
|
+
except Exception as ex:
|
|
151
|
+
raise LGHorizonApiConnectionError from ex
|
|
152
|
+
auth_json = await auth_response.json()
|
|
153
|
+
if not auth_response.ok:
|
|
154
|
+
error = None
|
|
155
|
+
if "error" in auth_json:
|
|
156
|
+
error = auth_json["error"]
|
|
157
|
+
if error and error["statusCode"] == 97401:
|
|
158
|
+
raise LGHorizonApiUnauthorizedError("Invalid credentials")
|
|
159
|
+
elif error:
|
|
160
|
+
raise LGHorizonApiConnectionError(error["message"])
|
|
161
|
+
else:
|
|
162
|
+
raise LGHorizonApiConnectionError("Unknown connection error")
|
|
163
|
+
|
|
164
|
+
self.household_id = auth_json["householdId"]
|
|
165
|
+
self.access_token = auth_json["accessToken"]
|
|
166
|
+
self.refresh_token = auth_json["refreshToken"]
|
|
167
|
+
self.username = auth_json["username"]
|
|
168
|
+
self.token_expiry = auth_json["refreshTokenExpiry"]
|
|
169
|
+
|
|
170
|
+
@backoff.on_exception(backoff.expo, LGHorizonApiConnectionError, max_tries=3)
|
|
171
|
+
async def request(self, host: str, path: str, params=None, **kwargs) -> Any:
|
|
172
|
+
"""Make a request."""
|
|
173
|
+
if headers := kwargs.pop("headers", {}):
|
|
174
|
+
headers = dict(headers)
|
|
175
|
+
request_url = f"{host}{path}"
|
|
176
|
+
if await self.is_token_expiring(): # Use property
|
|
177
|
+
_LOGGER.debug("Access token is expiring, fetching a new one")
|
|
178
|
+
await self.fetch_access_token()
|
|
179
|
+
try:
|
|
180
|
+
web_response = await self.websession.request(
|
|
181
|
+
"GET", request_url, **kwargs, headers=headers, params=params
|
|
182
|
+
)
|
|
183
|
+
web_response.raise_for_status()
|
|
184
|
+
json_response = await web_response.json()
|
|
185
|
+
_LOGGER.debug(
|
|
186
|
+
"Response from %s:\n %s",
|
|
187
|
+
request_url,
|
|
188
|
+
json.dumps(json_response, indent=2),
|
|
189
|
+
)
|
|
190
|
+
return json_response
|
|
191
|
+
except ClientResponseError as cre:
|
|
192
|
+
_LOGGER.error("Error response from %s: %s", request_url, str(cre))
|
|
193
|
+
if cre.status == 401:
|
|
194
|
+
await self.fetch_access_token()
|
|
195
|
+
raise LGHorizonApiConnectionError(
|
|
196
|
+
f"Unable to call {request_url}. Error:{str(cre)}"
|
|
197
|
+
) from cre
|
|
198
|
+
|
|
199
|
+
except Exception as ex:
|
|
200
|
+
_LOGGER.error("Error calling %s: %s", request_url, str(ex))
|
|
201
|
+
raise LGHorizonApiConnectionError(
|
|
202
|
+
f"Unable to call {request_url}. Error:{str(ex)}"
|
|
203
|
+
) from ex
|
|
204
|
+
|
|
205
|
+
async def get_mqtt_token(self) -> Any:
|
|
206
|
+
"""Get the MQTT token."""
|
|
207
|
+
_LOGGER.debug("Fetching MQTT token")
|
|
208
|
+
config = await self.get_service_config()
|
|
209
|
+
service_url = await config.get_service_url("authorizationService")
|
|
210
|
+
result = await self.request(
|
|
211
|
+
service_url,
|
|
212
|
+
"/v1/mqtt/token",
|
|
213
|
+
)
|
|
214
|
+
return result["token"]
|
|
215
|
+
|
|
216
|
+
async def get_service_config(self):
|
|
217
|
+
"""Get the service configuration."""
|
|
218
|
+
_LOGGER.debug("Fetching service configuration")
|
|
219
|
+
if self._service_config is None: # Use property and backing field
|
|
220
|
+
base_country_code = self.country_code[0:2]
|
|
221
|
+
result = await self.request(
|
|
222
|
+
self._host,
|
|
223
|
+
f"/{base_country_code}/en/config-service/conf/web/backoffice.json",
|
|
224
|
+
)
|
|
225
|
+
self._service_config = LGHorizonServicesConfig(result)
|
|
226
|
+
|
|
227
|
+
return self._service_config
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""LG Horizon Channel model."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LGHorizonChannel:
|
|
5
|
+
"""Class to represent a channel."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, channel_json):
|
|
8
|
+
"""Initialize a channel."""
|
|
9
|
+
self.channel_json = channel_json
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def id(self) -> str:
|
|
13
|
+
"""Returns the id."""
|
|
14
|
+
return self.channel_json["id"]
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def channel_number(self) -> str:
|
|
18
|
+
"""Returns the channel number."""
|
|
19
|
+
return self.channel_json["logicalChannelNumber"]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def is_radio(self) -> bool:
|
|
23
|
+
"""Returns if the channel is a radio channel."""
|
|
24
|
+
return self.channel_json.get("isRadio", False)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def title(self) -> str:
|
|
28
|
+
"""Returns the title."""
|
|
29
|
+
return self.channel_json["name"]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def logo_image(self) -> str:
|
|
33
|
+
"""Returns the logo image."""
|
|
34
|
+
if "logo" in self.channel_json and "focused" in self.channel_json["logo"]:
|
|
35
|
+
return self.channel_json["logo"]["focused"]
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def linear_products(self) -> list[str]:
|
|
40
|
+
"""Returns the linear products."""
|
|
41
|
+
return self.channel_json.get("linearProducts", [])
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def stream_image(self) -> str:
|
|
45
|
+
"""Returns the stream image."""
|
|
46
|
+
image_stream = self.channel_json["imageStream"]
|
|
47
|
+
if "full" in image_stream:
|
|
48
|
+
return image_stream["full"]
|
|
49
|
+
if "small" in image_stream:
|
|
50
|
+
return image_stream["small"]
|
|
51
|
+
if "logo" in self.channel_json and "focused" in self.channel_json["logo"]:
|
|
52
|
+
return self.channel_json["logo"]["focused"]
|
|
53
|
+
return ""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Configuration handler for LG Horizon services."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LGHorizonServicesConfig:
|
|
7
|
+
"""Handle LG Horizon configuration and service URLs."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, config_data: dict[str, Any]) -> None:
|
|
10
|
+
"""Initialize LG Horizon config.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
config_data: Configuration dictionary with service endpoints
|
|
14
|
+
"""
|
|
15
|
+
self._config = config_data
|
|
16
|
+
|
|
17
|
+
async def get_service_url(self, service_name: str) -> str:
|
|
18
|
+
"""Get the URL for a specific service.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
service_name: Name of the service (e.g., 'authService', 'recordingService')
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
URL for the service
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If the service or its URL is not found
|
|
28
|
+
"""
|
|
29
|
+
if service_name in self._config and "URL" in self._config[service_name]:
|
|
30
|
+
return self._config[service_name]["URL"]
|
|
31
|
+
raise ValueError(f"Service URL for '{service_name}' not found in configuration")
|
|
32
|
+
|
|
33
|
+
async def get_all_services(self) -> dict[str, str]:
|
|
34
|
+
"""Get all available services and their URLs.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dictionary mapping service names to URLs
|
|
38
|
+
"""
|
|
39
|
+
return {
|
|
40
|
+
name: url
|
|
41
|
+
for name, service in self._config.items()
|
|
42
|
+
if isinstance(service, dict) and (url := service.get("URL"))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async def __getattr__(self, name: str) -> Optional[str]:
|
|
46
|
+
"""Access service URLs as attributes.
|
|
47
|
+
|
|
48
|
+
Example: config.authService returns the auth service URL
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name: Service name
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
URL for the service or None if not found
|
|
55
|
+
"""
|
|
56
|
+
if name.startswith("_"):
|
|
57
|
+
raise AttributeError(
|
|
58
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
59
|
+
)
|
|
60
|
+
return await self.get_service_url(name)
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
"""Return string representation."""
|
|
64
|
+
services = list(self._config.keys())
|
|
65
|
+
return f"LGHorizonConfig({len(services)} services)"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""LGHorizon customer model."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from .lghorizon_profile import LGHorizonProfile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LGHorizonCustomer:
|
|
8
|
+
"""LGHorizon customer."""
|
|
9
|
+
|
|
10
|
+
_profiles: Dict[str, LGHorizonProfile] = {}
|
|
11
|
+
|
|
12
|
+
def __init__(self, json_payload: dict):
|
|
13
|
+
"""Initialize a customer."""
|
|
14
|
+
self._json_payload = json_payload
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def customer_id(self) -> str:
|
|
18
|
+
"""Return the customer id."""
|
|
19
|
+
return self._json_payload["customerId"]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def hashed_customer_id(self) -> str:
|
|
23
|
+
"""Return the hashed customer id."""
|
|
24
|
+
return self._json_payload["hashedCustomerId"]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def country_id(self) -> str:
|
|
28
|
+
"""Return the country id."""
|
|
29
|
+
return self._json_payload["countryId"]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def city_id(self) -> int:
|
|
33
|
+
"""Return the city id."""
|
|
34
|
+
return self._json_payload["cityId"]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def assigned_devices(self) -> list[str]:
|
|
38
|
+
"""Return the assigned set-top boxes."""
|
|
39
|
+
return self._json_payload.get("assignedDevices", [])
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def profiles(self) -> Dict[str, LGHorizonProfile]:
|
|
43
|
+
"""Return the profiles."""
|
|
44
|
+
if not self._profiles or self._profiles == {}:
|
|
45
|
+
self._profiles = {
|
|
46
|
+
p["profileId"]: LGHorizonProfile(p)
|
|
47
|
+
for p in self._json_payload.get("profiles", [])
|
|
48
|
+
}
|
|
49
|
+
return self._profiles
|
|
50
|
+
|
|
51
|
+
async def get_profile_lang(self, profile_id: str) -> str:
|
|
52
|
+
"""Return the profile language."""
|
|
53
|
+
if profile_id not in self.profiles:
|
|
54
|
+
return "nl"
|
|
55
|
+
return self.profiles[profile_id].options.lang
|
|
@@ -1,38 +1,44 @@
|
|
|
1
|
-
"""LG Horizon
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
1
|
+
"""LG Horizon device (set-top box) model."""
|
|
4
2
|
|
|
5
3
|
import json
|
|
6
4
|
import logging
|
|
7
|
-
from typing import
|
|
8
|
-
from .lghorizon_models import (
|
|
9
|
-
LGHorizonRunningState,
|
|
10
|
-
LGHorizonStatusMessage,
|
|
11
|
-
LGHorizonUIStatusMessage,
|
|
12
|
-
LGHorizonDeviceState,
|
|
13
|
-
LGHorizonAuth,
|
|
14
|
-
LGHorizonChannel,
|
|
15
|
-
)
|
|
5
|
+
from typing import Callable, Dict, Optional, Any, Coroutine
|
|
16
6
|
|
|
17
|
-
|
|
18
|
-
from
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
|
|
8
|
+
from ..const import (
|
|
9
|
+
ONLINE_RUNNING,
|
|
10
|
+
MEDIA_KEY_POWER,
|
|
11
|
+
MEDIA_KEY_PLAY_PAUSE,
|
|
12
|
+
MEDIA_KEY_STOP,
|
|
23
13
|
MEDIA_KEY_CHANNEL_UP,
|
|
14
|
+
MEDIA_KEY_CHANNEL_DOWN,
|
|
24
15
|
MEDIA_KEY_ENTER,
|
|
16
|
+
MEDIA_KEY_REWIND,
|
|
25
17
|
MEDIA_KEY_FAST_FORWARD,
|
|
26
|
-
MEDIA_KEY_PLAY_PAUSE,
|
|
27
|
-
MEDIA_KEY_POWER,
|
|
28
18
|
MEDIA_KEY_RECORD,
|
|
29
|
-
MEDIA_KEY_REWIND,
|
|
30
|
-
MEDIA_KEY_STOP,
|
|
31
|
-
ONLINE_RUNNING,
|
|
32
19
|
PLATFORM_TYPES,
|
|
33
20
|
)
|
|
21
|
+
from ..helpers import make_id
|
|
22
|
+
from .lghorizon_auth import LGHorizonAuth
|
|
23
|
+
from .lghorizon_channel import LGHorizonChannel
|
|
24
|
+
from .lghorizon_mqtt_client import LGHorizonMqttClient # Added import for type checking
|
|
25
|
+
from .lghorizon_device_state import LGHorizonDeviceState, LGHorizonRunningState
|
|
26
|
+
from .exceptions import LGHorizonApiConnectionError
|
|
27
|
+
from .lghorizon_message import LGHorizonStatusMessage, LGHorizonUIStatusMessage
|
|
28
|
+
|
|
29
|
+
from ..device_state_processor import LGHorizonDeviceStateProcessor
|
|
30
|
+
|
|
31
|
+
# Assuming these models are available from legacy or will be moved to models/
|
|
32
|
+
# from ..legacy.models import (
|
|
33
|
+
# # LGHorizonPlayingInfo,
|
|
34
|
+
# # LGHorizonPlayerState, # This is now in lghorizon_ui_status.py
|
|
35
|
+
# # LGHorizonReplayEvent,
|
|
36
|
+
# # LGHorizonRecordingSingle,
|
|
37
|
+
# # LGHorizonVod,
|
|
38
|
+
# # LGHorizonApp,
|
|
39
|
+
# )
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
_logger = logging.getLogger(__name__)
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class LGHorizonDevice:
|
|
@@ -201,7 +207,7 @@ class LGHorizonDevice:
|
|
|
201
207
|
|
|
202
208
|
async def _trigger_callback(self):
|
|
203
209
|
if self._change_callback:
|
|
204
|
-
|
|
210
|
+
_logger.debug("Callback called from box %s", self.device_id)
|
|
205
211
|
await self._change_callback(self.device_id)
|
|
206
212
|
|
|
207
213
|
async def turn_on(self) -> None:
|