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.
@@ -1,539 +1,300 @@
1
- """Python client for LGHorizon."""
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 json
7
- import re
8
-
9
- from typing import Any, Callable, Dict, List
10
- import backoff
11
-
12
- from requests import Session, exceptions as request_exceptions
13
-
14
- from .exceptions import (
15
- LGHorizonApiUnauthorizedError,
16
- LGHorizonApiConnectionError,
17
- LGHorizonApiLockedError,
18
- )
19
-
20
- from .models import (
21
- LGHorizonAuth,
22
- LGHorizonBox,
23
- LGHorizonMqttClient,
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
- _logger = logging.getLogger(__name__)
50
- _supported_platforms = ["EOS", "EOS2", "HORIZON", "APOLLO"]
27
+ _LOGGER = logging.getLogger(__name__)
51
28
 
52
29
 
53
30
  class LGHorizonApi:
54
- """Main class for handling connections with LGHorizon Settop boxes."""
55
-
56
- _auth: LGHorizonAuth = None
57
- _session: Session = None
58
- settop_boxes: Dict[str, LGHorizonBox] = None
59
- customer: LGHorizonCustomer = None
60
- _mqtt_client: LGHorizonMqttClient = None
61
- _channels: Dict[str, LGHorizonChannel] = None
62
- _country_settings = None
63
- _country_code: str = None
64
- recording_capacity: int = None
65
- _entitlements: List[str] = None
66
- _identifier: str = None
67
- _config: str = None
68
- _refresh_callback: Callable = None
69
- _profile_id: str = None
70
-
71
- def __init__(
72
- self,
73
- username: str,
74
- password: str,
75
- country_code: str = "nl",
76
- identifier: str = None,
77
- refresh_token=None,
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
- def _authorize(self) -> None:
95
- ctry_code = self._country_code[0:2]
96
- if ctry_code in ("gb", "ch", "be"):
97
- self._authorize_with_refresh_token()
98
- else:
99
- self._authorize_default()
100
-
101
- def _authorize_default(self) -> None:
102
- _logger.debug("Authorizing")
103
- auth_url = f"{self._country_settings['api_url']}/auth-service/v1/authorization"
104
- auth_headers = {"x-device-code": "web"}
105
- auth_payload = {"password": self.password, "username": self.username}
106
- try:
107
- auth_response = self._session.post(
108
- auth_url, headers=auth_headers, json=auth_payload
109
- )
110
- except Exception as ex:
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
- headers = {"content-type": "application/json", "charset": "utf-8"}
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
- # Step 2 - Get Authorization cookie
187
- _logger.debug("Step 2 - Get Authorization cookie")
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
- auth_cookie_response = login_session.get(authorization_uri)
190
- if not auth_cookie_response.ok:
191
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
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
- _logger.debug("Step 3 - Login")
88
+ return self._devices
194
89
 
195
- username_fieldname = self._country_settings["oauth_username_fieldname"]
196
- pasword_fieldname = self._country_settings["oauth_password_fieldname"]
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
- payload = {
199
- username_fieldname: self.username,
200
- pasword_fieldname: self.password,
201
- "rememberme": "true",
202
- }
95
+ return self._customer.profiles
203
96
 
204
- login_response = login_session.post(
205
- self._country_settings["oauth_url"], payload, allow_redirects=False
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
- 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",
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
- post_result = login_session.post(
233
- auth_url, json.dumps(new_payload), headers=headers
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._auth.fill(post_result.json())
236
- self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
237
- except Exception:
238
- pass
239
-
240
- def _obtain_mqtt_token(self):
241
- _logger.debug("Obtain mqtt token...")
242
- mqtt_auth_url = self._config["authorizationService"]["URL"]
243
- mqtt_response = self._do_api_call(f"{mqtt_auth_url}/v1/mqtt/token")
244
- self._auth.mqttToken = mqtt_response["token"]
245
- _logger.debug("MQTT token: %s", self._auth.mqttToken)
246
-
247
- @backoff.on_exception(
248
- backoff.expo,
249
- BaseException,
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
- self._register_customer_and_boxes()
271
- self._mqtt_client.connect()
272
-
273
- def disconnect(self):
274
- """Disconnect."""
275
- _logger.debug("Disconnect from API")
276
- if not self._mqtt_client or not self._mqtt_client.is_connected:
277
- return
278
- self._mqtt_client.disconnect()
279
-
280
- def _on_mqtt_connected(self) -> None:
281
- _logger.debug("Connected to MQTT server. Registering all boxes...")
282
- box: LGHorizonBox
283
- for box in self.settop_boxes.values():
284
- box.register_mqtt()
285
-
286
- def _on_mqtt_message(self, message: str, topic: str) -> None:
287
- if "action" in message and message["action"] == "OPS.getProfilesUpdate":
288
- self._update_customer()
289
- elif "source" in message:
290
- device_id = message["source"]
291
- if not isinstance(device_id, str):
292
- _logger.debug("ignoring message - not a string")
293
- return
294
- if device_id not in self.settop_boxes:
295
- return
296
- try:
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
- _logger.debug("Personalisation result: %s ", personalisation_result)
430
- self.customer = LGHorizonCustomer(personalisation_result)
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
- for channel in channels_result:
439
- if "isRadio" in channel and channel["isRadio"]:
440
- continue
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["linearProducts"])
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
- def get_display_channels(self):
451
- """Returns channels to display baed on profile."""
452
- all_channels = self._channels.values()
453
- if not self._profile_id or self._profile_id not in self.customer.profiles:
454
- return all_channels
455
- profile_channel_ids = self.customer.profiles[self._profile_id].favorite_channels
456
- if len(profile_channel_ids) == 0:
457
- return all_channels
458
-
459
- return [
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 get_recording_show(self, show_id: str) -> list[LGHorizonRecordingSingle]:
511
- """Returns show recording"""
512
- _logger.info("Retrieving show recordings...")
513
- show_recording_content = self._do_api_call(
514
- f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/episodes/shows/{show_id}?source=recording&language=nl&sort=time&sortOrder=asc"
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 _update_entitlements(self) -> None:
526
- _logger.info("Retrieving entitlements...")
527
- entitlements_json = self._do_api_call(
528
- f"{self._config['purchaseService']['URL']}/v2/customers/{self._auth.household_id}/entitlements?enableDaypass=true"
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
- self._entitlements.clear()
531
- for entitlement in entitlements_json["entitlements"]:
532
- self._entitlements.append(entitlement["id"])
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"]