lghorizon 0.9.0.dev4__py3-none-any.whl → 0.9.1__py3-none-any.whl
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/__init__.py +58 -25
- lghorizon/const.py +23 -81
- lghorizon/exceptions.py +3 -3
- lghorizon/helpers.py +1 -1
- lghorizon/lghorizon_api.py +263 -502
- lghorizon/lghorizon_device.py +409 -0
- lghorizon/lghorizon_device_state_processor.py +365 -0
- lghorizon/lghorizon_message_factory.py +38 -0
- lghorizon/lghorizon_models.py +1512 -0
- lghorizon/lghorizon_mqtt_client.py +335 -0
- lghorizon/lghorizon_recording_factory.py +55 -0
- lghorizon-0.9.1.dist-info/METADATA +189 -0
- lghorizon-0.9.1.dist-info/RECORD +17 -0
- lghorizon/models.py +0 -768
- lghorizon-0.9.0.dev4.dist-info/METADATA +0 -41
- lghorizon-0.9.0.dev4.dist-info/RECORD +0 -12
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/WHEEL +0 -0
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/top_level.txt +0 -0
lghorizon/lghorizon_api.py
CHANGED
|
@@ -1,539 +1,300 @@
|
|
|
1
|
-
"""
|
|
2
|
-
# pylint: disable=broad-exception-caught
|
|
3
|
-
# pylint: disable=line-too-long
|
|
1
|
+
"""LG Horizon API client."""
|
|
4
2
|
|
|
5
3
|
import logging
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
from .
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
from .
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
LGHorizonCustomer,
|
|
25
|
-
LGHorizonChannel,
|
|
26
|
-
LGHorizonReplayEvent,
|
|
27
|
-
LGHorizonRecordingSingle,
|
|
28
|
-
LGHorizonVod,
|
|
29
|
-
LGHorizonApp,
|
|
30
|
-
LGHorizonBaseRecording,
|
|
31
|
-
LGHorizonRecordingListSeasonShow,
|
|
32
|
-
LGHorizonRecordingEpisode,
|
|
33
|
-
LGHorizonRecordingShow,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
from .const import (
|
|
37
|
-
COUNTRY_SETTINGS,
|
|
38
|
-
BOX_PLAY_STATE_BUFFER,
|
|
39
|
-
BOX_PLAY_STATE_CHANNEL,
|
|
40
|
-
BOX_PLAY_STATE_DVR,
|
|
41
|
-
BOX_PLAY_STATE_REPLAY,
|
|
42
|
-
BOX_PLAY_STATE_VOD,
|
|
43
|
-
RECORDING_TYPE_SINGLE,
|
|
44
|
-
RECORDING_TYPE_SEASON,
|
|
45
|
-
RECORDING_TYPE_SHOW,
|
|
4
|
+
from typing import Any, Dict, cast, Callable, Optional
|
|
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 (
|
|
19
|
+
LGHorizonRecordingList,
|
|
20
|
+
LGHorizonRecordingQuota,
|
|
21
|
+
LGHorizonShowRecordingList,
|
|
46
22
|
)
|
|
23
|
+
from .lghorizon_recording_factory import LGHorizonRecordingFactory
|
|
24
|
+
from .lghorizon_device_state_processor import LGHorizonDeviceStateProcessor
|
|
47
25
|
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
_supported_platforms = ["EOS", "EOS2", "HORIZON", "APOLLO"]
|
|
27
|
+
_LOGGER = logging.getLogger(__name__)
|
|
51
28
|
|
|
52
29
|
|
|
53
30
|
class LGHorizonApi:
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
profile_id=None,
|
|
79
|
-
) -> None:
|
|
80
|
-
"""Create LGHorizon API."""
|
|
81
|
-
self.username = username
|
|
82
|
-
self.password = password
|
|
83
|
-
self.refresh_token = refresh_token
|
|
84
|
-
self._session = Session()
|
|
85
|
-
self._country_settings = COUNTRY_SETTINGS[country_code]
|
|
86
|
-
self._country_code = country_code
|
|
87
|
-
self._auth = LGHorizonAuth()
|
|
88
|
-
self.settop_boxes = {}
|
|
89
|
-
self._channels = {}
|
|
90
|
-
self._entitlements = []
|
|
91
|
-
self._identifier = identifier
|
|
31
|
+
"""LG Horizon API client."""
|
|
32
|
+
|
|
33
|
+
_mqtt_client: LGHorizonMqttClient | None
|
|
34
|
+
auth: LGHorizonAuth
|
|
35
|
+
_service_config: LGHorizonServicesConfig
|
|
36
|
+
_customer: LGHorizonCustomer
|
|
37
|
+
_channels: Dict[str, LGHorizonChannel]
|
|
38
|
+
_entitlements: LGHorizonEntitlements
|
|
39
|
+
_profile_id: str
|
|
40
|
+
_initialized: bool = False
|
|
41
|
+
_devices: Dict[str, LGHorizonDevice] = {}
|
|
42
|
+
_message_factory: LGHorizonMessageFactory = LGHorizonMessageFactory()
|
|
43
|
+
_device_state_processor: LGHorizonDeviceStateProcessor | None
|
|
44
|
+
_recording_factory: LGHorizonRecordingFactory = LGHorizonRecordingFactory()
|
|
45
|
+
|
|
46
|
+
def __init__(self, auth: LGHorizonAuth, profile_id: str = "") -> None:
|
|
47
|
+
"""Initialize LG Horizon API client."""
|
|
48
|
+
"""Initialize LG Horizon API client.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
auth: The authentication object for API requests.
|
|
52
|
+
profile_id: The ID of the user profile to use (optional).
|
|
53
|
+
"""
|
|
54
|
+
self.auth = auth
|
|
92
55
|
self._profile_id = profile_id
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
raise LGHorizonApiConnectionError("Unknown connection failure") from ex
|
|
112
|
-
|
|
113
|
-
if not auth_response.ok:
|
|
114
|
-
error_json = auth_response.json()
|
|
115
|
-
error = error_json["error"]
|
|
116
|
-
if error and error["statusCode"] == 97401:
|
|
117
|
-
raise LGHorizonApiUnauthorizedError("Invalid credentials")
|
|
118
|
-
elif error and error["statusCode"] == 97117:
|
|
119
|
-
raise LGHorizonApiLockedError("Account locked")
|
|
120
|
-
elif error:
|
|
121
|
-
raise LGHorizonApiConnectionError(error["message"])
|
|
122
|
-
else:
|
|
123
|
-
raise LGHorizonApiConnectionError("Unknown connection error")
|
|
124
|
-
|
|
125
|
-
self._auth.fill(auth_response.json())
|
|
126
|
-
_logger.debug("Authorization succeeded")
|
|
127
|
-
|
|
128
|
-
def _authorize_with_refresh_token(self) -> None:
|
|
129
|
-
"""Handle authorizzationg using request token."""
|
|
130
|
-
_logger.debug("Authorizing via refresh")
|
|
131
|
-
refresh_url = (
|
|
132
|
-
f"{self._country_settings['api_url']}/auth-service/v1/authorization/refresh"
|
|
56
|
+
self._channels = {}
|
|
57
|
+
self._device_state_processor = None
|
|
58
|
+
self._mqtt_client = None
|
|
59
|
+
self._initialized = False
|
|
60
|
+
|
|
61
|
+
async def initialize(self) -> None:
|
|
62
|
+
"""Initialize the API client."""
|
|
63
|
+
self._service_config = await self.auth.get_service_config()
|
|
64
|
+
self._customer = await self._get_customer_info()
|
|
65
|
+
if self._profile_id == "":
|
|
66
|
+
self._profile_id = list(self._customer.profiles.keys())[0]
|
|
67
|
+
await self._refresh_entitlements()
|
|
68
|
+
await self._refresh_channels()
|
|
69
|
+
self._mqtt_client = await self._create_mqtt_client()
|
|
70
|
+
await self._mqtt_client.connect()
|
|
71
|
+
await self._register_devices()
|
|
72
|
+
self._device_state_processor = LGHorizonDeviceStateProcessor(
|
|
73
|
+
self.auth, self._channels, self._customer, self._profile_id
|
|
133
74
|
)
|
|
134
|
-
|
|
135
|
-
payload = '{"refreshToken":"' + self.refresh_token + '"}'
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
auth_response = self._session.post(
|
|
139
|
-
refresh_url, headers=headers, data=payload
|
|
140
|
-
)
|
|
141
|
-
except Exception as ex:
|
|
142
|
-
raise LGHorizonApiConnectionError("Unknown connection failure") from ex
|
|
143
|
-
|
|
144
|
-
if not auth_response.ok:
|
|
145
|
-
_logger.debug("response %s", auth_response)
|
|
146
|
-
error_json = auth_response.json()
|
|
147
|
-
error = None
|
|
148
|
-
if "error" in error_json:
|
|
149
|
-
error = error_json["error"]
|
|
150
|
-
if error and error["statusCode"] == 97401:
|
|
151
|
-
raise LGHorizonApiUnauthorizedError("Invalid credentials")
|
|
152
|
-
elif error:
|
|
153
|
-
raise LGHorizonApiConnectionError(error["message"])
|
|
154
|
-
else:
|
|
155
|
-
raise LGHorizonApiConnectionError("Unknown connection error")
|
|
156
|
-
|
|
157
|
-
self._auth.fill(auth_response.json())
|
|
158
|
-
self.refresh_token = self._auth.refresh_token
|
|
159
|
-
self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
|
|
160
|
-
|
|
161
|
-
if self._refresh_callback:
|
|
162
|
-
self._refresh_callback()
|
|
163
|
-
|
|
164
|
-
_logger.debug("Authorization succeeded")
|
|
165
|
-
|
|
166
|
-
def set_callback(self, refresh_callback: Callable) -> None:
|
|
167
|
-
"""Set the refresh callback."""
|
|
168
|
-
self._refresh_callback = refresh_callback
|
|
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"]
|
|
75
|
+
self._initialized = True
|
|
185
76
|
|
|
186
|
-
|
|
187
|
-
|
|
77
|
+
async def set_token_refresh_callback(
|
|
78
|
+
self, token_refresh_callback: Callable[str, None]
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Set the token refresh callback."""
|
|
81
|
+
self.auth.token_refresh_callback = token_refresh_callback
|
|
188
82
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
83
|
+
async def get_devices(self) -> dict[str, LGHorizonDevice]:
|
|
84
|
+
"""Get devices."""
|
|
85
|
+
if not self._initialized:
|
|
86
|
+
raise RuntimeError("LGHorizonApi not initialized")
|
|
192
87
|
|
|
193
|
-
|
|
88
|
+
return self._devices
|
|
194
89
|
|
|
195
|
-
|
|
196
|
-
|
|
90
|
+
async def get_profiles(self) -> dict[str, LGHorizonProfile]:
|
|
91
|
+
"""Get profile IDs."""
|
|
92
|
+
if not self._initialized:
|
|
93
|
+
raise RuntimeError("LGHorizonApi not initialized")
|
|
197
94
|
|
|
198
|
-
|
|
199
|
-
username_fieldname: self.username,
|
|
200
|
-
pasword_fieldname: self.password,
|
|
201
|
-
"rememberme": "true",
|
|
202
|
-
}
|
|
95
|
+
return self._customer.profiles
|
|
203
96
|
|
|
204
|
-
|
|
205
|
-
|
|
97
|
+
async def get_profile_channels(
|
|
98
|
+
self, profile_id: Optional[str] = None
|
|
99
|
+
) -> Dict[str, LGHorizonChannel]:
|
|
100
|
+
"""Returns channels to display baed on profile."""
|
|
101
|
+
# Attempt to retrieve the profile by the given profile_id
|
|
102
|
+
if not profile_id:
|
|
103
|
+
profile_id = self._profile_id
|
|
104
|
+
profile = self._customer.profiles.get(profile_id)
|
|
105
|
+
|
|
106
|
+
# If the specified profile is not found, and there are other profiles available,
|
|
107
|
+
# default to the first profile in the customer's list if available.
|
|
108
|
+
if not profile and self._customer.profiles:
|
|
109
|
+
_LOGGER.debug(
|
|
110
|
+
"Profile with ID '%s' not found. Defaulting to first available profile.",
|
|
111
|
+
profile_id,
|
|
206
112
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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",
|
|
113
|
+
profile = list(self._customer.profiles.values())[0]
|
|
114
|
+
|
|
115
|
+
# If a profile is found and it has favorite channels, filter the main channels list.
|
|
116
|
+
if profile and profile.favorite_channels:
|
|
117
|
+
_LOGGER.debug("Returning favorite channels for profile '%s'.", profile.name)
|
|
118
|
+
# Use a set for faster lookup of favorite channel IDs
|
|
119
|
+
profile_channel_ids = set(profile.favorite_channels)
|
|
120
|
+
return {
|
|
121
|
+
channel.id: channel
|
|
122
|
+
for channel in self._channels.values()
|
|
123
|
+
if channel.id in profile_channel_ids
|
|
231
124
|
}
|
|
232
|
-
|
|
233
|
-
|
|
125
|
+
|
|
126
|
+
# If no profile is found (even after defaulting) or the profile has no favorite channels,
|
|
127
|
+
# return all available channels.
|
|
128
|
+
_LOGGER.debug("No specific profile channels found, returning all channels.")
|
|
129
|
+
return self._channels
|
|
130
|
+
|
|
131
|
+
async def _register_devices(self) -> None:
|
|
132
|
+
"""Register devices."""
|
|
133
|
+
_LOGGER.debug("Registering devices...")
|
|
134
|
+
self._devices = {}
|
|
135
|
+
channels = await self.get_profile_channels(self._profile_id)
|
|
136
|
+
for raw_box in self._customer.assigned_devices:
|
|
137
|
+
_LOGGER.debug("Creating box for device: %s", raw_box)
|
|
138
|
+
if self._device_state_processor is None:
|
|
139
|
+
self._device_state_processor = LGHorizonDeviceStateProcessor(
|
|
140
|
+
self.auth, self._channels, self._customer, self._profile_id
|
|
141
|
+
)
|
|
142
|
+
device = LGHorizonDevice(
|
|
143
|
+
raw_box,
|
|
144
|
+
self._mqtt_client,
|
|
145
|
+
self._device_state_processor,
|
|
146
|
+
self.auth,
|
|
147
|
+
channels,
|
|
234
148
|
)
|
|
235
|
-
self.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
jitter=None,
|
|
251
|
-
max_tries=3,
|
|
252
|
-
logger=_logger,
|
|
253
|
-
giveup=lambda e: isinstance(
|
|
254
|
-
e, (LGHorizonApiLockedError, LGHorizonApiUnauthorizedError)
|
|
255
|
-
),
|
|
256
|
-
)
|
|
257
|
-
def connect(self) -> None:
|
|
258
|
-
"""Start connection process."""
|
|
259
|
-
self._config = self._get_config(self._country_code)
|
|
260
|
-
_logger.debug("Connect to API")
|
|
261
|
-
self._authorize()
|
|
262
|
-
self._obtain_mqtt_token()
|
|
263
|
-
self._mqtt_client = LGHorizonMqttClient(
|
|
264
|
-
self._auth,
|
|
265
|
-
self._config["mqttBroker"]["URL"],
|
|
149
|
+
self._devices[device.device_id] = device
|
|
150
|
+
|
|
151
|
+
async def disconnect(self) -> None:
|
|
152
|
+
"""Disconnect the client."""
|
|
153
|
+
if self._mqtt_client:
|
|
154
|
+
await self._mqtt_client.disconnect()
|
|
155
|
+
self._initialized = False
|
|
156
|
+
|
|
157
|
+
async def _create_mqtt_client(self) -> LGHorizonMqttClient:
|
|
158
|
+
"""Create and configure the MQTT client.
|
|
159
|
+
|
|
160
|
+
Returns: An initialized LGHorizonMqttClient instance.
|
|
161
|
+
"""
|
|
162
|
+
mqtt_client = await LGHorizonMqttClient.create(
|
|
163
|
+
self.auth,
|
|
266
164
|
self._on_mqtt_connected,
|
|
267
165
|
self._on_mqtt_message,
|
|
268
166
|
)
|
|
167
|
+
return mqtt_client
|
|
168
|
+
|
|
169
|
+
async def _on_mqtt_connected(self):
|
|
170
|
+
"""MQTT connected callback."""
|
|
171
|
+
await self._mqtt_client.subscribe("#")
|
|
172
|
+
await self._mqtt_client.subscribe(self.auth.household_id)
|
|
173
|
+
# await self._mqtt_client.subscribe(self.auth.household_id + "/#")
|
|
174
|
+
# await self._mqtt_client.subscribe(self.auth.household_id + "/+/#")
|
|
175
|
+
await self._mqtt_client.subscribe(
|
|
176
|
+
self.auth.household_id + "/" + self._mqtt_client.client_id
|
|
177
|
+
)
|
|
178
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/+/status")
|
|
179
|
+
await self._mqtt_client.subscribe(
|
|
180
|
+
self.auth.household_id + "/+/networkRecordings"
|
|
181
|
+
)
|
|
182
|
+
await self._mqtt_client.subscribe(
|
|
183
|
+
self.auth.household_id + "/+/networkRecordings/capacity"
|
|
184
|
+
)
|
|
185
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/+/localRecordings")
|
|
186
|
+
await self._mqtt_client.subscribe(
|
|
187
|
+
self.auth.household_id + "/+/localRecordings/capacity"
|
|
188
|
+
)
|
|
189
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/watchlistService")
|
|
190
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/purchaseService")
|
|
191
|
+
await self._mqtt_client.subscribe(
|
|
192
|
+
self.auth.household_id + "/personalizationService"
|
|
193
|
+
)
|
|
194
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/recordingStatus")
|
|
195
|
+
await self._mqtt_client.subscribe(
|
|
196
|
+
self.auth.household_id + "/recordingStatus/lastUserAction"
|
|
197
|
+
)
|
|
269
198
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if "deviceType" in message and message["deviceType"] == "STB":
|
|
298
|
-
self.settop_boxes[device_id].update_state(message)
|
|
299
|
-
if "status" in message:
|
|
300
|
-
self._handle_box_update(device_id, message)
|
|
301
|
-
|
|
302
|
-
except Exception:
|
|
303
|
-
_logger.exception("Could not handle status message")
|
|
304
|
-
_logger.warning("Full message: %s", str(message))
|
|
305
|
-
self.settop_boxes[device_id].playing_info.reset()
|
|
306
|
-
self.settop_boxes[device_id].playing_info.set_paused(False)
|
|
307
|
-
elif "CPE.capacity" in message:
|
|
308
|
-
splitted_topic = topic.split("/")
|
|
309
|
-
if len(splitted_topic) != 4:
|
|
310
|
-
return
|
|
311
|
-
device_id = splitted_topic[1]
|
|
312
|
-
if device_id not in self.settop_boxes:
|
|
313
|
-
return
|
|
314
|
-
self.settop_boxes[device_id].update_recording_capacity(message)
|
|
315
|
-
|
|
316
|
-
def _handle_box_update(self, device_id: str, raw_message: Any) -> None:
|
|
317
|
-
status_payload = raw_message["status"]
|
|
318
|
-
if "uiStatus" not in status_payload:
|
|
319
|
-
return
|
|
320
|
-
ui_status = status_payload["uiStatus"]
|
|
321
|
-
if ui_status == "mainUI":
|
|
322
|
-
player_state = status_payload["playerState"]
|
|
323
|
-
if "sourceType" not in player_state or "source" not in player_state:
|
|
324
|
-
return
|
|
325
|
-
source_type = player_state["sourceType"]
|
|
326
|
-
state_source = player_state["source"]
|
|
327
|
-
self.settop_boxes[device_id].playing_info.set_paused(
|
|
328
|
-
player_state["speed"] == 0
|
|
329
|
-
)
|
|
330
|
-
if (
|
|
331
|
-
source_type
|
|
332
|
-
in (
|
|
333
|
-
BOX_PLAY_STATE_CHANNEL,
|
|
334
|
-
BOX_PLAY_STATE_BUFFER,
|
|
335
|
-
BOX_PLAY_STATE_REPLAY,
|
|
336
|
-
)
|
|
337
|
-
and "eventId" in state_source
|
|
338
|
-
):
|
|
339
|
-
event_id = state_source["eventId"]
|
|
340
|
-
raw_replay_event = self._do_api_call(
|
|
341
|
-
f"{self._config['linearService']['URL']}/v2/replayEvent/{event_id}?returnLinearContent=true&language={self._country_settings['language']}"
|
|
342
|
-
)
|
|
343
|
-
replay_event = LGHorizonReplayEvent(raw_replay_event)
|
|
344
|
-
channel = self._channels[replay_event.channel_id]
|
|
345
|
-
self.settop_boxes[device_id].update_with_replay_event(
|
|
346
|
-
source_type, replay_event, channel
|
|
347
|
-
)
|
|
348
|
-
elif source_type == BOX_PLAY_STATE_DVR:
|
|
349
|
-
recording_id = state_source["recordingId"]
|
|
350
|
-
session_start_time = state_source["sessionStartTime"]
|
|
351
|
-
session_end_time = state_source["sessionEndTime"]
|
|
352
|
-
last_speed_change_time = player_state["lastSpeedChangeTime"]
|
|
353
|
-
relative_position = player_state["relativePosition"]
|
|
354
|
-
raw_recording = self._do_api_call(
|
|
355
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/details/single/{recording_id}?profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&language={self._country_settings['language']}"
|
|
356
|
-
)
|
|
357
|
-
recording = LGHorizonRecordingSingle(raw_recording)
|
|
358
|
-
channel = self._channels[recording.channel_id]
|
|
359
|
-
self.settop_boxes[device_id].update_with_recording(
|
|
360
|
-
source_type,
|
|
361
|
-
recording,
|
|
362
|
-
channel,
|
|
363
|
-
session_start_time,
|
|
364
|
-
session_end_time,
|
|
365
|
-
last_speed_change_time,
|
|
366
|
-
relative_position,
|
|
367
|
-
)
|
|
368
|
-
elif source_type == BOX_PLAY_STATE_VOD:
|
|
369
|
-
title_id = state_source["titleId"]
|
|
370
|
-
last_speed_change_time = player_state["lastSpeedChangeTime"]
|
|
371
|
-
relative_position = player_state["relativePosition"]
|
|
372
|
-
raw_vod = self._do_api_call(
|
|
373
|
-
f"{self._config['vodService']['URL']}/v2/detailscreen/{title_id}?language={self._country_settings['language']}&profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&cityId={self.customer.city_id}"
|
|
374
|
-
)
|
|
375
|
-
vod = LGHorizonVod(raw_vod)
|
|
376
|
-
self.settop_boxes[device_id].update_with_vod(
|
|
377
|
-
source_type, vod, last_speed_change_time, relative_position
|
|
378
|
-
)
|
|
379
|
-
elif ui_status == "apps":
|
|
380
|
-
app = LGHorizonApp(status_payload["appsState"])
|
|
381
|
-
self.settop_boxes[device_id].update_with_app("app", app)
|
|
382
|
-
|
|
383
|
-
@backoff.on_exception(
|
|
384
|
-
backoff.expo, LGHorizonApiConnectionError, max_tries=3, logger=_logger
|
|
385
|
-
)
|
|
386
|
-
def _do_api_call(self, url: str) -> str:
|
|
387
|
-
_logger.info("Executing API call to %s", url)
|
|
388
|
-
try:
|
|
389
|
-
api_response = self._session.get(url)
|
|
390
|
-
api_response.raise_for_status()
|
|
391
|
-
json_response = api_response.json()
|
|
392
|
-
except request_exceptions.HTTPError as http_ex:
|
|
393
|
-
self._authorize()
|
|
394
|
-
raise LGHorizonApiConnectionError(
|
|
395
|
-
f"Unable to call {url}. Error:{str(http_ex)}"
|
|
396
|
-
) from http_ex
|
|
397
|
-
_logger.debug("Result API call: %s", json_response)
|
|
398
|
-
return json_response
|
|
399
|
-
|
|
400
|
-
def _register_customer_and_boxes(self):
|
|
401
|
-
self._update_customer()
|
|
402
|
-
self._get_channels()
|
|
403
|
-
if len(self.customer.settop_boxes) == 0:
|
|
404
|
-
_logger.warning("No boxes found.")
|
|
405
|
-
return
|
|
406
|
-
_logger.info("Registering boxes")
|
|
407
|
-
for device in self.customer.settop_boxes:
|
|
408
|
-
platform_type = device["platformType"]
|
|
409
|
-
if platform_type not in _supported_platforms:
|
|
410
|
-
continue
|
|
411
|
-
if (
|
|
412
|
-
"platform_types" in self._country_settings
|
|
413
|
-
and platform_type in self._country_settings["platform_types"]
|
|
414
|
-
):
|
|
415
|
-
platform_type = self._country_settings["platform_types"][platform_type]
|
|
416
|
-
else:
|
|
417
|
-
platform_type = None
|
|
418
|
-
box = LGHorizonBox(
|
|
419
|
-
device, platform_type, self._mqtt_client, self._auth, self._channels
|
|
420
|
-
)
|
|
421
|
-
self.settop_boxes[box.device_id] = box
|
|
422
|
-
_logger.info("Box %s registered...", box.device_id)
|
|
423
|
-
|
|
424
|
-
def _update_customer(self):
|
|
425
|
-
_logger.info("Get customer data")
|
|
426
|
-
personalisation_result = self._do_api_call(
|
|
427
|
-
f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.household_id}?with=profiles%2Cdevices"
|
|
199
|
+
async def _on_mqtt_message(self, mqtt_message: dict, mqtt_topic: str):
|
|
200
|
+
"""MQTT message callback."""
|
|
201
|
+
message = await self._message_factory.create_message(mqtt_topic, mqtt_message)
|
|
202
|
+
match message.message_type:
|
|
203
|
+
case LGHorizonMessageType.STATUS:
|
|
204
|
+
message.__class__ = LGHorizonStatusMessage
|
|
205
|
+
status_message = cast(LGHorizonStatusMessage, message)
|
|
206
|
+
device = self._devices.get(status_message.source, None)
|
|
207
|
+
if not device:
|
|
208
|
+
return
|
|
209
|
+
await device.handle_status_message(status_message)
|
|
210
|
+
case LGHorizonMessageType.UI_STATUS:
|
|
211
|
+
message.__class__ = LGHorizonUIStatusMessage
|
|
212
|
+
ui_status_message = cast(LGHorizonUIStatusMessage, message)
|
|
213
|
+
device = self._devices.get(ui_status_message.source, None)
|
|
214
|
+
if not device:
|
|
215
|
+
return
|
|
216
|
+
if (
|
|
217
|
+
not device.device_state.state
|
|
218
|
+
== LGHorizonRunningState.ONLINE_RUNNING
|
|
219
|
+
):
|
|
220
|
+
return
|
|
221
|
+
await device.handle_ui_status_message(ui_status_message)
|
|
222
|
+
|
|
223
|
+
async def _get_customer_info(self) -> LGHorizonCustomer:
|
|
224
|
+
service_url = await self._service_config.get_service_url(
|
|
225
|
+
"personalizationService"
|
|
428
226
|
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def _get_channels(self):
|
|
433
|
-
self._update_entitlements()
|
|
434
|
-
_logger.info("Retrieving channels...")
|
|
435
|
-
channels_result = self._do_api_call(
|
|
436
|
-
f"{self._config['linearService']['URL']}/v2/channels?cityId={self.customer.city_id}&language={self._country_settings['language']}&productClass=Orion-DASH"
|
|
227
|
+
result = await self.auth.request(
|
|
228
|
+
service_url,
|
|
229
|
+
f"/v1/customer/{self.auth.household_id}?with=profiles%2Cdevices",
|
|
437
230
|
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
231
|
+
return LGHorizonCustomer(result)
|
|
232
|
+
|
|
233
|
+
async def _refresh_entitlements(self) -> Any:
|
|
234
|
+
"""Retrieve entitlements."""
|
|
235
|
+
_LOGGER.debug("Retrieving entitlements...")
|
|
236
|
+
service_url = await self._service_config.get_service_url("purchaseService")
|
|
237
|
+
result = await self.auth.request(
|
|
238
|
+
service_url,
|
|
239
|
+
f"/v2/customers/{self.auth.household_id}/entitlements?enableDaypass=true",
|
|
240
|
+
)
|
|
241
|
+
self._entitlements = LGHorizonEntitlements(result)
|
|
242
|
+
|
|
243
|
+
async def _refresh_channels(self):
|
|
244
|
+
"""Retrieve channels."""
|
|
245
|
+
_LOGGER.debug("Retrieving channels...")
|
|
246
|
+
service_url = await self._service_config.get_service_url("linearService")
|
|
247
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
248
|
+
channels_json = await self.auth.request(
|
|
249
|
+
service_url,
|
|
250
|
+
f"/v2/channels?cityId={self._customer.city_id}&language={lang}&productClass=Orion-DASH",
|
|
251
|
+
)
|
|
252
|
+
for channel_json in channels_json:
|
|
253
|
+
channel = LGHorizonChannel(channel_json)
|
|
441
254
|
common_entitlements = list(
|
|
442
|
-
set(self._entitlements) & set(channel
|
|
255
|
+
set(self._entitlements.entitlement_ids) & set(channel.linear_products)
|
|
443
256
|
)
|
|
257
|
+
|
|
444
258
|
if len(common_entitlements) == 0:
|
|
445
259
|
continue
|
|
446
|
-
channel_id = channel["id"]
|
|
447
|
-
self._channels[channel_id] = LGHorizonChannel(channel)
|
|
448
|
-
_logger.info("%s retrieved.", len(self._channels))
|
|
449
260
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
channel for channel in all_channels if channel.id in profile_channel_ids
|
|
461
|
-
]
|
|
462
|
-
|
|
463
|
-
def _get_replay_event(self, listing_id) -> Any:
|
|
464
|
-
"""Get listing."""
|
|
465
|
-
_logger.info("Retrieving replay event details...")
|
|
466
|
-
response = self._do_api_call(
|
|
467
|
-
f"{self._config['linearService']['URL']}/v2/replayEvent/{listing_id}?returnLinearContent=true&language={self._country_settings['language']}"
|
|
468
|
-
)
|
|
469
|
-
_logger.info("Replay event details retrieved")
|
|
470
|
-
return response
|
|
471
|
-
|
|
472
|
-
def get_recording_capacity(self) -> int:
|
|
473
|
-
"""Returns remaining recording capacity"""
|
|
474
|
-
ctry_code = self._country_code[0:2]
|
|
475
|
-
if ctry_code == "gb":
|
|
476
|
-
_logger.debug("GB: not supported")
|
|
477
|
-
return None
|
|
478
|
-
try:
|
|
479
|
-
_logger.info("Retrieving recordingcapacity...")
|
|
480
|
-
quota_content = self._do_api_call(
|
|
481
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/quota"
|
|
482
|
-
)
|
|
483
|
-
if "quota" not in quota_content and "occupied" not in quota_content:
|
|
484
|
-
_logger.error("Unable to fetch recording capacity...")
|
|
485
|
-
return None
|
|
486
|
-
capacity = (quota_content["occupied"] / quota_content["quota"]) * 100
|
|
487
|
-
self.recording_capacity = round(capacity)
|
|
488
|
-
_logger.debug("Remaining recordingcapacity %s %%", self.recording_capacity)
|
|
489
|
-
return self.recording_capacity
|
|
490
|
-
except Exception:
|
|
491
|
-
_logger.error("Unable to fetch recording capacity...")
|
|
492
|
-
return None
|
|
493
|
-
|
|
494
|
-
def get_recordings(self) -> List[LGHorizonBaseRecording]:
|
|
495
|
-
"""Returns recordings."""
|
|
496
|
-
_logger.info("Retrieving recordings...")
|
|
497
|
-
recording_content = self._do_api_call(
|
|
498
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/recordings?sort=time&sortOrder=desc&language={self._country_settings['language']}"
|
|
261
|
+
self._channels[channel.id] = channel
|
|
262
|
+
|
|
263
|
+
async def get_all_recordings(self) -> LGHorizonRecordingList:
|
|
264
|
+
"""Retrieve all recordings."""
|
|
265
|
+
_LOGGER.debug("Retrieving recordings...")
|
|
266
|
+
service_url = await self._service_config.get_service_url("recordingService")
|
|
267
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
268
|
+
recordings_json = await self.auth.request(
|
|
269
|
+
service_url,
|
|
270
|
+
f"/customers/{self.auth.household_id}/recordings?isAdult=false&offset=0&limit=100&sort=time&sortOrder=desc&profileId={self._profile_id}&language={lang}",
|
|
499
271
|
)
|
|
500
|
-
recordings =
|
|
501
|
-
for recording_data_item in recording_content["data"]:
|
|
502
|
-
recording_type = recording_data_item["type"]
|
|
503
|
-
if recording_type == RECORDING_TYPE_SINGLE:
|
|
504
|
-
recordings.append(LGHorizonRecordingSingle(recording_data_item))
|
|
505
|
-
elif recording_type in (RECORDING_TYPE_SEASON, RECORDING_TYPE_SHOW):
|
|
506
|
-
recordings.append(LGHorizonRecordingListSeasonShow(recording_data_item))
|
|
507
|
-
_logger.info("%s recordings retrieved...", len(recordings))
|
|
272
|
+
recordings = await self._recording_factory.create_recordings(recordings_json)
|
|
508
273
|
return recordings
|
|
509
274
|
|
|
510
|
-
def
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
275
|
+
async def get_show_recordings(
|
|
276
|
+
self, show_id: str, channel_id: str
|
|
277
|
+
) -> LGHorizonShowRecordingList: # type: ignore[valid-type]
|
|
278
|
+
"""Retrieve all recordings."""
|
|
279
|
+
_LOGGER.debug("Retrieving recordings fro show...")
|
|
280
|
+
service_url = await self._service_config.get_service_url("recordingService")
|
|
281
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
282
|
+
episodes_json = await self.auth.request(
|
|
283
|
+
service_url,
|
|
284
|
+
f"/customers/{self.auth.household_id}/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",
|
|
515
285
|
)
|
|
516
|
-
recordings =
|
|
517
|
-
for item in show_recording_content["data"]:
|
|
518
|
-
if item["source"] == "show":
|
|
519
|
-
recordings.append(LGHorizonRecordingShow(item))
|
|
520
|
-
else:
|
|
521
|
-
recordings.append(LGHorizonRecordingEpisode(item))
|
|
522
|
-
_logger.info("%s showrecordings retrieved...", len(recordings))
|
|
286
|
+
recordings = await self._recording_factory.create_episodes(episodes_json)
|
|
523
287
|
return recordings
|
|
524
288
|
|
|
525
|
-
def
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
289
|
+
async def get_recording_quota(self) -> LGHorizonRecordingQuota:
|
|
290
|
+
"""Refresh recording quota."""
|
|
291
|
+
_LOGGER.debug("Refreshing recording quota...")
|
|
292
|
+
service_url = await self._service_config.get_service_url("recordingService")
|
|
293
|
+
quota_json = await self.auth.request(
|
|
294
|
+
service_url,
|
|
295
|
+
f"/customers/{self.auth.household_id}/quota",
|
|
529
296
|
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def _get_config(self, country_code: str):
|
|
535
|
-
base_country_code = country_code[0:2]
|
|
536
|
-
config_url = f"{self._country_settings['api_url']}/{base_country_code}/en/config-service/conf/web/backoffice.json"
|
|
537
|
-
result = self._do_api_call(config_url)
|
|
538
|
-
_logger.debug(result)
|
|
539
|
-
return result
|
|
297
|
+
return LGHorizonRecordingQuota(quota_json)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
__all__ = ["LGHorizonApi", "LGHorizonAuth"]
|