lghorizon 0.9.0__py3-none-any.whl → 0.9.0.dev1__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,1512 +0,0 @@
1
- """LG Horizon Model."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import logging
7
- import time
8
- from abc import ABC, abstractmethod
9
- from datetime import datetime
10
- from enum import Enum
11
- from typing import Any, Dict, List, Optional, Callable
12
-
13
- import backoff
14
- from aiohttp import ClientResponseError, ClientSession
15
-
16
- from .const import (
17
- COUNTRY_SETTINGS,
18
- )
19
- from .exceptions import LGHorizonApiConnectionError, LGHorizonApiUnauthorizedError
20
-
21
-
22
- _LOGGER = logging.getLogger(__name__)
23
-
24
-
25
- class LGHorizonRunningState(Enum):
26
- """Running state of horizon box."""
27
-
28
- UNKNOWN = "UNKNOWN"
29
- ONLINE_RUNNING = "ONLINE_RUNNING"
30
- ONLINE_STANDBY = "ONLINE_STANDBY"
31
-
32
-
33
- class LGHorizonMessageType(Enum):
34
- """Enumeration of LG Horizon message types."""
35
-
36
- UNKNOWN = 0
37
- STATUS = 1
38
- UI_STATUS = 2
39
-
40
-
41
- class LGHorizonRecordingSource(Enum):
42
- """LGHorizon recording."""
43
-
44
- SHOW = "show"
45
- UNKNOWN = "unknown"
46
-
47
-
48
- class LGHorizonRecordingState(Enum):
49
- """Enumeration of LG Horizon recording states."""
50
-
51
- RECORDED = "recorded"
52
- UNKNOWN = "unknown"
53
-
54
-
55
- class LGHorizonRecordingType(Enum): # type: ignore[no-redef]
56
- """Enumeration of LG Horizon recording states."""
57
-
58
- SINGLE = "single"
59
- SEASON = "season"
60
- SHOW = "show"
61
- UNKNOWN = "unknown"
62
-
63
-
64
- class LGHorizonUIStateType(Enum):
65
- """Enumeration of LG Horizon UI State types."""
66
-
67
- MAINUI = "mainUI"
68
- APPS = "apps"
69
- UNKNOWN = "unknown"
70
-
71
-
72
- class LGHorizonMessage(ABC):
73
- """Abstract base class for LG Horizon messages."""
74
-
75
- @property
76
- def topic(self) -> str:
77
- """Return the topic of the message."""
78
- return self._topic
79
-
80
- @property
81
- def payload(self) -> dict:
82
- """Return the payload of the message."""
83
- return self._payload
84
-
85
- @property
86
- @abstractmethod
87
- def message_type(self) -> LGHorizonMessageType | None:
88
- """Return the message type."""
89
-
90
- @abstractmethod
91
- def __init__(self, topic: str, payload: dict) -> None:
92
- """Abstract base class for LG Horizon messages."""
93
- self._topic = topic
94
- """Initialize the abstract base class for LG Horizon messages.
95
-
96
- Args:
97
- topic: The MQTT topic of the message.
98
- payload: The dictionary payload of the message.
99
- """
100
- self._payload = payload
101
-
102
- def __repr__(self) -> str:
103
- """Return a string representation of the message."""
104
- return f"LGHorizonStatusMessage(topic='{self._topic}', payload={json.dumps(self._payload, indent=2)})"
105
-
106
-
107
- class LGHorizonStatusMessage(LGHorizonMessage):
108
- """Represents an LG Horizon status message received via MQTT."""
109
-
110
- def __init__(self, payload: dict, topic: str) -> None:
111
- """Initialize an LG Horizon status message."""
112
- super().__init__(topic, payload)
113
-
114
- @property
115
- def message_type(self) -> LGHorizonMessageType:
116
- """Return the message type from the payload, if available."""
117
- return LGHorizonMessageType.STATUS
118
-
119
- @property
120
- def source(self) -> str:
121
- """Return the device ID from the payload, if available."""
122
- return self._payload.get("source", "unknown")
123
-
124
- @property
125
- def running_state(self) -> LGHorizonRunningState:
126
- """Return the device ID from the payload, if available."""
127
- return LGHorizonRunningState[self._payload.get("state", "unknown").upper()]
128
-
129
-
130
- class LGHorizonSourceType(Enum):
131
- """Enumeration of LG Horizon source types."""
132
-
133
- LINEAR = "linear"
134
- REVIEWBUFFER = "reviewBuffer"
135
- NDVR = "nDVR"
136
- REPLAY = "replay"
137
- VOD = "VOD"
138
- UNKNOWN = "unknown"
139
-
140
-
141
- class LGHorizonSource(ABC):
142
- """Abstract base class for LG Horizon sources."""
143
-
144
- def __init__(self, raw_json: dict) -> None:
145
- """Initialize the LG Horizon source."""
146
- self._raw_json = raw_json
147
-
148
- @property
149
- @abstractmethod
150
- def source_type(self) -> LGHorizonSourceType: # type: ignore[no-redef]
151
- """Return the message type."""
152
-
153
-
154
- class LGHorizonLinearSource(LGHorizonSource):
155
- """Represent the Linear Source of an LG Horizon device."""
156
-
157
- @property
158
- def channel_id(self) -> str:
159
- """Return the source type."""
160
- return self._raw_json.get("channelId", "")
161
-
162
- @property
163
- def event_id(self) -> str:
164
- """Return the event ID."""
165
- return self._raw_json.get("eventId", "")
166
-
167
- @property
168
- def source_type(self) -> LGHorizonSourceType:
169
- return LGHorizonSourceType.LINEAR
170
-
171
-
172
- class LGHorizonReviewBufferSource(LGHorizonSource):
173
- """Represent the ReviewBuffer Source of an LG Horizon device."""
174
-
175
- @property
176
- def channel_id(self) -> str:
177
- """Return the source type."""
178
- return self._raw_json.get("channelId", "")
179
-
180
- @property
181
- def event_id(self) -> str:
182
- """Return the event ID."""
183
- return self._raw_json.get("eventId", "")
184
-
185
- @property
186
- def source_type(self) -> LGHorizonSourceType:
187
- return LGHorizonSourceType.REVIEWBUFFER
188
-
189
-
190
- class LGHorizonNDVRSource(LGHorizonSource):
191
- """Represent the Network Digital Video Recorder (NDVR) Source of an LG Horizon device."""
192
-
193
- @property
194
- def recording_id(self) -> str:
195
- """Return the recording ID."""
196
- return self._raw_json.get("recordingId", "")
197
-
198
- @property
199
- def channel_id(self) -> str:
200
- """Return the channel ID."""
201
- return self._raw_json.get("channelId", "")
202
-
203
- @property
204
- def source_type(self) -> LGHorizonSourceType:
205
- return LGHorizonSourceType.NDVR
206
-
207
-
208
- class LGHorizonVODSource(LGHorizonSource):
209
- """Represent the VOD Source of an LG Horizon device."""
210
-
211
- @property
212
- def title_id(self) -> str:
213
- """Return the title ID."""
214
- return self._raw_json.get("titleId", "")
215
-
216
- @property
217
- def start_intro_time(self) -> int:
218
- """Return the start intro time."""
219
- return self._raw_json.get("startIntroTime", 0)
220
-
221
- @property
222
- def end_intro_time(self) -> int:
223
- """Return the end intro time."""
224
- return self._raw_json.get("endIntroTime", 0)
225
-
226
- @property
227
- def source_type(self) -> LGHorizonSourceType:
228
- return LGHorizonSourceType.VOD
229
-
230
-
231
- class LGHorizonReplaySource(LGHorizonSource):
232
- """Represent the Replay Source of an LG Horizon device."""
233
-
234
- @property
235
- def event_id(self) -> str:
236
- """Return the title ID."""
237
- return self._raw_json.get("eventId", "")
238
-
239
- @property
240
- def source_type(self) -> LGHorizonSourceType:
241
- """Return the source type."""
242
- return LGHorizonSourceType.REPLAY
243
-
244
-
245
- class LGHorizonUnknownSource(LGHorizonSource):
246
- """Represent an unknown source type of an LG Horizon device."""
247
-
248
- @property
249
- def source_type(self) -> LGHorizonSourceType:
250
- return LGHorizonSourceType.UNKNOWN
251
-
252
-
253
- class LGHorizonPlayerState:
254
- """Represent the Player State of an LG Horizon device."""
255
-
256
- def __init__(self, raw_json: dict) -> None:
257
- """Initialize the Player State."""
258
- self._raw_json = raw_json
259
-
260
- @property
261
- def source_type(self) -> LGHorizonSourceType:
262
- """Return the source type."""
263
- return LGHorizonSourceType[self._raw_json.get("sourceType", "unknown").upper()]
264
-
265
- @property
266
- def speed(self) -> int:
267
- """Return the Player State dictionary."""
268
- return self._raw_json.get("speed", 0)
269
-
270
- @property
271
- def last_speed_change_time(
272
- self,
273
- ) -> int:
274
- """Return the last speed change time."""
275
- return self._raw_json.get("lastSpeedChangeTime", 0.0)
276
-
277
- @property
278
- def relative_position(
279
- self,
280
- ) -> int:
281
- """Return the last speed change time."""
282
- return self._raw_json.get("relativePosition", 0.0)
283
-
284
- @property
285
- def source(self) -> LGHorizonSource | None: # Added None to the return type
286
- """Return the last speed change time."""
287
- if "source" in self._raw_json:
288
- match self.source_type:
289
- case LGHorizonSourceType.LINEAR:
290
- return LGHorizonLinearSource(self._raw_json["source"])
291
- case LGHorizonSourceType.VOD:
292
- return LGHorizonVODSource(self._raw_json["source"])
293
- case LGHorizonSourceType.REPLAY:
294
- return LGHorizonReplaySource(self._raw_json["source"])
295
- case LGHorizonSourceType.NDVR:
296
- return LGHorizonNDVRSource(self._raw_json["source"])
297
- case LGHorizonSourceType.REVIEWBUFFER:
298
- return LGHorizonReviewBufferSource(self._raw_json["source"])
299
-
300
- return LGHorizonUnknownSource(self._raw_json["source"])
301
-
302
-
303
- class LGHorizonAppsState:
304
- """Represent the Apps State of an LG Horizon device."""
305
-
306
- def __init__(self, raw_json: dict) -> None:
307
- """Initialize the Apps state."""
308
- self._raw_json = raw_json
309
-
310
- @property
311
- def id(self) -> str:
312
- """Return the id."""
313
- return self._raw_json.get("id", "")
314
-
315
- @property
316
- def app_name(self) -> str:
317
- """Return the app name."""
318
- return self._raw_json.get("appName", "")
319
-
320
- @property
321
- def logo_path(self) -> str:
322
- """Return the logo path."""
323
- return self._raw_json.get("logoPath", "")
324
-
325
-
326
- class LGHorizonUIState:
327
- """Represent the UI State of an LG Horizon device."""
328
-
329
- _player_state: LGHorizonPlayerState | None = None
330
- _apps_state: LGHorizonAppsState | None = None
331
-
332
- def __init__(self, raw_json: dict) -> None:
333
- """Initialize the State."""
334
- self._raw_json = raw_json
335
- """Initialize the UI State.
336
-
337
- Args:
338
- raw_json: The raw JSON dictionary containing UI state information.
339
- """
340
-
341
- @property
342
- def ui_status(self) -> LGHorizonUIStateType:
343
- """Return the UI status dictionary."""
344
- return LGHorizonUIStateType[self._raw_json.get("uiStatus", "unknown").upper()]
345
-
346
- @property
347
- def player_state(
348
- self,
349
- ) -> LGHorizonPlayerState | None: # Added None to the return type
350
- """Return the UI status dictionary."""
351
- # Check if _player_state is None and if "playerState" key exists in raw_json
352
- if self._player_state is None and "playerState" in self._raw_json:
353
- self._player_state = LGHorizonPlayerState(
354
- self._raw_json["playerState"]
355
- ) # Access directly as existence is checked
356
- return self._player_state
357
-
358
- @property
359
- def apps_state(
360
- self,
361
- ) -> LGHorizonAppsState | None: # Added None to the return type
362
- """Return the UI status dictionary."""
363
- # Check if _player_state is None and if "playerState" key exists in raw_json
364
- if self._apps_state is None and "appsState" in self._raw_json:
365
- self._apps_state = LGHorizonAppsState(
366
- self._raw_json["appsState"]
367
- ) # Access directly as existence is checked
368
- return self._apps_state
369
-
370
-
371
- class LGHorizonUIStatusMessage(LGHorizonMessage):
372
- """Represents an LG Horizon UI status message received via MQTT."""
373
-
374
- _status: LGHorizonUIState | None = None
375
-
376
- def __init__(self, payload: dict, topic: str) -> None:
377
- """Initialize an LG Horizon UI status message."""
378
- super().__init__(topic, payload)
379
-
380
- @property
381
- def message_type(self) -> LGHorizonMessageType:
382
- """Return the message type from the payload, if available."""
383
- return LGHorizonMessageType.UI_STATUS
384
-
385
- @property
386
- def source(self) -> str:
387
- """Return the device ID from the payload, if available."""
388
- return self._payload.get("source", "unknown")
389
-
390
- @property
391
- def message_timestamp(self) -> int:
392
- """Return the device ID from the payload, if available."""
393
- return self._payload.get("messageTimeStamp", 0)
394
-
395
- @property
396
- def ui_state(self) -> LGHorizonUIState | None:
397
- """Return the device ID from the payload, if available."""
398
- if not self._status and "status" in self._payload:
399
- self._status = LGHorizonUIState(self._payload["status"])
400
- return self._status
401
-
402
-
403
- class LGHorizonUnknownMessage(LGHorizonMessage):
404
- """Represents an unknown LG Horizon message received via MQTT."""
405
-
406
- def __init__(self, payload: dict, topic: str) -> None:
407
- """Initialize an LG Horizon unknown message."""
408
- super().__init__(topic, payload)
409
-
410
- @property
411
- def message_type(self) -> LGHorizonMessageType:
412
- """Return the message type from the payload, if available."""
413
- return LGHorizonMessageType.UNKNOWN
414
-
415
-
416
- class LGHorizonProfileOptions:
417
- """LGHorizon profile options."""
418
-
419
- def __init__(self, options_payload: dict):
420
- """Initialize a profile options."""
421
- self._options_payload = options_payload
422
-
423
- @property
424
- def lang(self) -> str:
425
- """Return the language."""
426
- return self._options_payload["lang"]
427
-
428
-
429
- class LGHorizonProfile:
430
- """LGHorizon profile."""
431
-
432
- _options: LGHorizonProfileOptions
433
- _profile_payload: dict
434
-
435
- def __init__(self, profile_payload: dict):
436
- """Initialize a profile."""
437
- self._profile_payload = profile_payload
438
- self._options = LGHorizonProfileOptions(self._profile_payload["options"])
439
-
440
- @property
441
- def id(self) -> str:
442
- """Return the profile id."""
443
- return self._profile_payload["profileId"]
444
-
445
- @property
446
- def name(self) -> str:
447
- """Return the profile name."""
448
- return self._profile_payload["name"]
449
-
450
- @property
451
- def favorite_channels(self) -> list[str]:
452
- """Return the favorite channels."""
453
- return self._profile_payload.get("favoriteChannels", [])
454
-
455
- @property
456
- def options(self) -> LGHorizonProfileOptions:
457
- """Return the profile options."""
458
- return self._options
459
-
460
-
461
- class LGHorizonAuth:
462
- """Class to make authenticated requests."""
463
-
464
- _websession: ClientSession
465
- _refresh_token: str
466
- _access_token: Optional[str]
467
- _username: str
468
- _password: str
469
- _household_id: str
470
- _token_expiry: Optional[int]
471
- _country_code: str
472
- _host: str
473
- _use_refresh_token: bool
474
- _token_refresh_callback: Callable[str, None] | None # pyright: ignore[reportInvalidTypeForm]
475
-
476
- def __init__(
477
- self,
478
- websession: ClientSession,
479
- country_code: str,
480
- refresh_token: str = "",
481
- username: str = "",
482
- password: str = "",
483
- ) -> None:
484
- """Initialize the auth with refresh token."""
485
- self._websession = websession
486
- self._refresh_token = refresh_token
487
- self._access_token = None
488
- self._username = username
489
- self._password = password
490
- self._household_id = ""
491
- self._token_expiry = None
492
- self._country_code = country_code
493
- self._host = COUNTRY_SETTINGS[country_code]["api_url"]
494
- self._use_refresh_token = COUNTRY_SETTINGS[country_code]["use_refreshtoken"]
495
- self._service_config = None
496
- self._token_refresh_callback = None
497
-
498
- @property
499
- def websession(self) -> ClientSession:
500
- """Return the aiohttp client session."""
501
- return self._websession
502
-
503
- @property
504
- def refresh_token(self) -> str:
505
- """Return the refresh token."""
506
- return self._refresh_token
507
-
508
- @refresh_token.setter
509
- def refresh_token(self, value: str) -> None:
510
- """Set the refresh token."""
511
- self._refresh_token = value
512
-
513
- @property
514
- def access_token(self) -> Optional[str]:
515
- """Return the access token."""
516
- return self._access_token
517
-
518
- @access_token.setter
519
- def access_token(self, value: Optional[str]) -> None:
520
- """Set the access token."""
521
- self._access_token = value
522
-
523
- @property
524
- def username(self) -> str:
525
- """Return the username."""
526
- return self._username
527
-
528
- @username.setter
529
- def username(self, value: str) -> None:
530
- """Set the username."""
531
- self._username = value
532
-
533
- @property
534
- def password(self) -> str:
535
- """Return the password."""
536
- return self._password
537
-
538
- @password.setter
539
- def password(self, value: str) -> None:
540
- """Set the password."""
541
- self._password = value
542
-
543
- @property
544
- def household_id(self) -> str:
545
- """Return the household ID."""
546
- return self._household_id
547
-
548
- @household_id.setter
549
- def household_id(self, value: str) -> None:
550
- """Set the household ID."""
551
- self._household_id = value
552
-
553
- @property
554
- def token_expiry(self) -> Optional[int]:
555
- """Return the token expiry timestamp."""
556
- return self._token_expiry
557
-
558
- @token_expiry.setter
559
- def token_expiry(self, value: Optional[int]) -> None:
560
- """Set the token expiry timestamp."""
561
- self._token_expiry = value
562
-
563
- @property
564
- def country_code(self) -> str:
565
- """Return the country code."""
566
- return self._country_code
567
-
568
- async def is_token_expiring(self) -> bool:
569
- """Check if the token is expiring within one day."""
570
- if not self.access_token or not self.token_expiry:
571
- return True
572
- current_unix_time = int(time.time())
573
- return current_unix_time >= (self.token_expiry - 86400)
574
-
575
- async def fetch_access_token(self) -> None:
576
- """Fetch the access token."""
577
- _LOGGER.debug("Fetching access token")
578
- headers = dict()
579
- headers["content-type"] = "application/json"
580
- headers["charset"] = "utf-8"
581
-
582
- if not self._use_refresh_token and self.access_token is None:
583
- payload = {"password": self.password, "username": self.username}
584
- headers["x-device-code"] = "web"
585
- auth_url_path = "/auth-service/v1/authorization"
586
- else:
587
- payload = {"refreshToken": self.refresh_token}
588
- auth_url_path = "/auth-service/v1/authorization/refresh"
589
- try: # Use properties and backing fields
590
- auth_response = await self.websession.post(
591
- f"{self._host}{auth_url_path}",
592
- json=payload,
593
- headers=headers,
594
- )
595
- except Exception as ex:
596
- raise LGHorizonApiConnectionError from ex
597
- auth_json = await auth_response.json()
598
- if not auth_response.ok:
599
- error = None
600
- if "error" in auth_json:
601
- error = auth_json["error"]
602
- if error and error["statusCode"] == 97401:
603
- raise LGHorizonApiUnauthorizedError("Invalid credentials")
604
- elif error:
605
- raise LGHorizonApiConnectionError(error["message"])
606
- else:
607
- raise LGHorizonApiConnectionError("Unknown connection error")
608
-
609
- self.household_id = auth_json["householdId"]
610
- self.access_token = auth_json["accessToken"]
611
- self.refresh_token = auth_json["refreshToken"]
612
- if self._token_refresh_callback:
613
- self._token_refresh_callback(self.refresh_token)
614
- self.username = auth_json["username"]
615
- self.token_expiry = auth_json["refreshTokenExpiry"]
616
-
617
- @backoff.on_exception(backoff.expo, LGHorizonApiConnectionError, max_tries=3)
618
- async def request(self, host: str, path: str, params=None, **kwargs) -> Any:
619
- """Make a request."""
620
- if headers := kwargs.pop("headers", {}):
621
- headers = dict(headers)
622
- request_url = f"{host}{path}"
623
- if await self.is_token_expiring(): # Use property
624
- _LOGGER.debug("Access token is expiring, fetching a new one")
625
- await self.fetch_access_token()
626
- try:
627
- web_response = await self.websession.request(
628
- "GET", request_url, **kwargs, headers=headers, params=params
629
- )
630
- web_response.raise_for_status()
631
- json_response = await web_response.json()
632
- _LOGGER.debug(
633
- "Response from %s:\n %s",
634
- request_url,
635
- json.dumps(json_response, indent=2),
636
- )
637
- return json_response
638
- except ClientResponseError as cre:
639
- _LOGGER.error("Error response from %s: %s", request_url, str(cre))
640
- if cre.status == 401:
641
- await self.fetch_access_token()
642
- raise LGHorizonApiConnectionError(
643
- f"Unable to call {request_url}. Error:{str(cre)}"
644
- ) from cre
645
-
646
- except Exception as ex:
647
- _LOGGER.error("Error calling %s: %s", request_url, str(ex))
648
- raise LGHorizonApiConnectionError(
649
- f"Unable to call {request_url}. Error:{str(ex)}"
650
- ) from ex
651
-
652
- async def get_mqtt_token(self) -> Any:
653
- """Get the MQTT token."""
654
- _LOGGER.debug("Fetching MQTT token")
655
- config = await self.get_service_config()
656
- service_url = await config.get_service_url("authorizationService")
657
- result = await self.request(
658
- service_url,
659
- "/v1/mqtt/token",
660
- )
661
- return result["token"]
662
-
663
- async def get_service_config(self):
664
- """Get the service configuration."""
665
- _LOGGER.debug("Fetching service configuration")
666
- if self._service_config is None: # Use property and backing field
667
- base_country_code = self.country_code[0:2]
668
- result = await self.request(
669
- self._host,
670
- f"/{base_country_code}/en/config-service/conf/web/backoffice.json",
671
- )
672
- self._service_config = LGHorizonServicesConfig(result)
673
-
674
- return self._service_config
675
-
676
-
677
- class LGHorizonChannel:
678
- """Class to represent a channel."""
679
-
680
- def __init__(self, channel_json):
681
- """Initialize a channel."""
682
- self.channel_json = channel_json
683
-
684
- @property
685
- def id(self) -> str:
686
- """Returns the id."""
687
- return self.channel_json["id"]
688
-
689
- @property
690
- def channel_number(self) -> str:
691
- """Returns the channel number."""
692
- return self.channel_json["logicalChannelNumber"]
693
-
694
- @property
695
- def replay_pre_padding(self) -> int:
696
- """Returns the channel number."""
697
- return self.channel_json.get("replayPrePadding", 0)
698
-
699
- @property
700
- def replay_post_padding(self) -> int:
701
- """Returns the channel number."""
702
- return self.channel_json.get("replayPostPadding", 0)
703
-
704
- @property
705
- def is_radio(self) -> bool:
706
- """Returns if the channel is a radio channel."""
707
- return self.channel_json.get("isRadio", False)
708
-
709
- @property
710
- def title(self) -> str:
711
- """Returns the title."""
712
- return self.channel_json["name"]
713
-
714
- @property
715
- def logo_image(self) -> str:
716
- """Returns the logo image."""
717
- if "logo" in self.channel_json and "focused" in self.channel_json["logo"]:
718
- return self.channel_json["logo"]["focused"]
719
- return ""
720
-
721
- @property
722
- def linear_products(self) -> list[str]:
723
- """Returns the linear products."""
724
- return self.channel_json.get("linearProducts", [])
725
-
726
- @property
727
- def stream_image(self) -> str:
728
- """Returns the stream image."""
729
- image_stream = self.channel_json["imageStream"]
730
- if "full" in image_stream:
731
- return image_stream["full"]
732
- if "small" in image_stream:
733
- return image_stream["small"]
734
- if "logo" in self.channel_json and "focused" in self.channel_json["logo"]:
735
- return self.channel_json["logo"]["focused"]
736
- return ""
737
-
738
-
739
- class LGHorizonServicesConfig:
740
- """Handle LG Horizon configuration and service URLs."""
741
-
742
- def __init__(self, config_data: dict[str, Any]) -> None:
743
- """Initialize LG Horizon config.
744
-
745
- Args:
746
- config_data: Configuration dictionary with service endpoints
747
- """
748
- self._config = config_data
749
-
750
- async def get_service_url(self, service_name: str) -> str:
751
- """Get the URL for a specific service.
752
-
753
- Args:
754
- service_name: Name of the service (e.g., 'authService', 'recordingService')
755
-
756
- Returns:
757
- URL for the service
758
-
759
- Raises:
760
- ValueError: If the service or its URL is not found
761
- """
762
- if service_name in self._config and "URL" in self._config[service_name]:
763
- return self._config[service_name]["URL"]
764
- raise ValueError(f"Service URL for '{service_name}' not found in configuration")
765
-
766
- async def get_all_services(self) -> dict[str, str]:
767
- """Get all available services and their URLs.
768
-
769
- Returns:
770
- Dictionary mapping service names to URLs
771
- """
772
- return {
773
- name: url
774
- for name, service in self._config.items()
775
- if isinstance(service, dict) and (url := service.get("URL"))
776
- }
777
-
778
- async def __getattr__(self, name: str) -> Optional[str]:
779
- """Access service URLs as attributes.
780
-
781
- Example: config.authService returns the auth service URL
782
-
783
- Args:
784
- name: Service name
785
-
786
- Returns:
787
- URL for the service or None if not found
788
- """
789
- if name.startswith("_"):
790
- raise AttributeError(
791
- f"'{type(self).__name__}' object has no attribute '{name}'"
792
- )
793
- return await self.get_service_url(name)
794
-
795
- def __repr__(self) -> str:
796
- """Return string representation."""
797
- services = list(self._config.keys())
798
- return f"LGHorizonConfig({len(services)} services)"
799
-
800
-
801
- class LGHorizonCustomer:
802
- """LGHorizon customer."""
803
-
804
- _profiles: Dict[str, LGHorizonProfile] = {}
805
-
806
- def __init__(self, json_payload: dict):
807
- """Initialize a customer."""
808
- self._json_payload = json_payload
809
-
810
- @property
811
- def customer_id(self) -> str:
812
- """Return the customer id."""
813
- return self._json_payload["customerId"]
814
-
815
- @property
816
- def hashed_customer_id(self) -> str:
817
- """Return the hashed customer id."""
818
- return self._json_payload["hashedCustomerId"]
819
-
820
- @property
821
- def country_id(self) -> str:
822
- """Return the country id."""
823
- return self._json_payload["countryId"]
824
-
825
- @property
826
- def city_id(self) -> int:
827
- """Return the city id."""
828
- return self._json_payload["cityId"]
829
-
830
- @property
831
- def assigned_devices(self) -> list[str]:
832
- """Return the assigned set-top boxes."""
833
- return self._json_payload.get("assignedDevices", [])
834
-
835
- @property
836
- def profiles(self) -> Dict[str, LGHorizonProfile]:
837
- """Return the profiles."""
838
- if not self._profiles or self._profiles == {}:
839
- self._profiles = {
840
- p["profileId"]: LGHorizonProfile(p)
841
- for p in self._json_payload.get("profiles", [])
842
- }
843
- return self._profiles
844
-
845
- async def get_profile_lang(self, profile_id: str) -> str:
846
- """Return the profile language."""
847
- if profile_id not in self.profiles:
848
- return "nl"
849
- return self.profiles[profile_id].options.lang
850
-
851
-
852
- class LGHorizonDeviceState:
853
- """Represent current state of a box."""
854
-
855
- _id: Optional[str]
856
- _channel_id: Optional[str]
857
- _channel_name: Optional[str]
858
- _show_title: Optional[str]
859
- _episode_title: Optional[str]
860
- _season_number: Optional[int]
861
- _episode_number: Optional[int]
862
- _image: Optional[str]
863
- _source_type: LGHorizonSourceType
864
- _paused: bool
865
- _duration: Optional[float]
866
- _position: Optional[float]
867
- _last_position_update: Optional[datetime]
868
- _state: LGHorizonRunningState
869
- _speed: Optional[int]
870
- _start_time: Optional[int]
871
- _end_time: Optional[int]
872
-
873
- def __init__(self) -> None:
874
- """Initialize the playing info."""
875
- self._channel_id = None
876
- self._show_title = None
877
- self._episode_title = None
878
- self._season_number = None
879
- self._episode_number = None
880
- self._image = None
881
- self._source_type = LGHorizonSourceType.UNKNOWN
882
- self._ui_state_type = LGHorizonUIStateType.UNKNOWN
883
- self._paused = False
884
- self._duration = None
885
- self._position = None
886
- self._last_position_update = None
887
- self._state = LGHorizonRunningState.UNKNOWN
888
- self._speed = None
889
- self._channel_name = None
890
- self._id = None
891
- self._start_time = None
892
- self._end_time = None
893
-
894
- @property
895
- def state(self) -> LGHorizonRunningState:
896
- """Return the channel ID."""
897
- return self._state
898
-
899
- @state.setter
900
- def state(self, value: LGHorizonRunningState) -> None:
901
- """Set the channel ID."""
902
- self._state = value
903
-
904
- @property
905
- def channel_id(self) -> Optional[str]:
906
- """Return the channel ID."""
907
- return self._channel_id
908
-
909
- @channel_id.setter
910
- def channel_id(self, value: Optional[str]) -> None:
911
- """Set the channel ID."""
912
- self._channel_id = value
913
-
914
- @property
915
- def id(self) -> Optional[str]:
916
- """Return the channel ID."""
917
- return self._id
918
-
919
- @id.setter
920
- def id(self, value: Optional[str]) -> None:
921
- """Set the channel ID."""
922
- self._id = value
923
-
924
- @property
925
- def channel_name(self) -> Optional[str]:
926
- """Return the channel ID."""
927
- return self._channel_name
928
-
929
- @channel_name.setter
930
- def channel_name(self, value: Optional[str]) -> None:
931
- """Set the channel ID."""
932
- self._channel_name = value
933
-
934
- @property
935
- def show_title(self) -> Optional[str]:
936
- """Return the title."""
937
- return self._show_title
938
-
939
- @show_title.setter
940
- def show_title(self, value: Optional[str]) -> None:
941
- """Set the title."""
942
- self._show_title = value
943
-
944
- @property
945
- def app_name(self) -> Optional[str]:
946
- """Return the title."""
947
- return self._app_name
948
-
949
- @app_name.setter
950
- def app_name(self, value: Optional[str]) -> None:
951
- """Set the title."""
952
- self._app_name = value
953
-
954
- @property
955
- def episode_title(self) -> Optional[str]:
956
- """Return the title."""
957
- return self._episode_title
958
-
959
- @episode_title.setter
960
- def episode_title(self, value: Optional[str]) -> None:
961
- """Set the title."""
962
- self._episode_title = value
963
-
964
- @property
965
- def episode_number(self) -> Optional[int]:
966
- """Return the title."""
967
- return self._episode_number
968
-
969
- @episode_number.setter
970
- def episode_number(self, value: Optional[int]) -> None:
971
- """Set the title."""
972
- self._episode_number = value
973
-
974
- @property
975
- def season_number(self) -> Optional[int]:
976
- """Return the title."""
977
- return self._season_number
978
-
979
- @season_number.setter
980
- def season_number(self, value: Optional[int]) -> None:
981
- """Set the title."""
982
- self._season_number = value
983
-
984
- @property
985
- def start_time(self) -> Optional[int]:
986
- """Return the title."""
987
- return self._start_time
988
-
989
- @start_time.setter
990
- def start_time(self, value: Optional[int]) -> None:
991
- """Set the title."""
992
- self._start_time = value
993
-
994
- @property
995
- def end_time(self) -> Optional[int]:
996
- """Return the title."""
997
- return self._end_time
998
-
999
- @end_time.setter
1000
- def end_time(self, value: Optional[int]) -> None:
1001
- """Set the title."""
1002
- self._end_time = value
1003
-
1004
- @property
1005
- def image(self) -> Optional[str]:
1006
- """Return the image URL."""
1007
- return self._image
1008
-
1009
- @image.setter
1010
- def image(self, value: Optional[str]) -> None:
1011
- """Set the image URL."""
1012
- self._image = value
1013
-
1014
- @property
1015
- def source_type(self) -> LGHorizonSourceType:
1016
- """Return the source type."""
1017
- return self._source_type
1018
-
1019
- @source_type.setter
1020
- def source_type(self, value: LGHorizonSourceType) -> None:
1021
- """Set the source type."""
1022
- self._source_type = value
1023
-
1024
- @property
1025
- def ui_state_type(self) -> LGHorizonUIStateType:
1026
- """Return the source type."""
1027
- return self._ui_state_type
1028
-
1029
- @ui_state_type.setter
1030
- def ui_state_type(self, value: LGHorizonUIStateType) -> None:
1031
- """Set the source type."""
1032
- self._ui_state_type = value
1033
-
1034
- @property
1035
- def paused(self) -> bool:
1036
- """Return if the media is paused."""
1037
- if self.speed is None:
1038
- return False
1039
- return self.speed == 0
1040
-
1041
- @property
1042
- def duration(self) -> Optional[float]:
1043
- """Return the duration of the media."""
1044
- return self._duration
1045
-
1046
- @duration.setter
1047
- def duration(self, value: Optional[float]) -> None:
1048
- """Set the duration of the media."""
1049
- self._duration = value
1050
-
1051
- @property
1052
- def position(self) -> Optional[float]:
1053
- """Return the current position in the media."""
1054
- return self._position
1055
-
1056
- @position.setter
1057
- def position(self, value: Optional[float]) -> None:
1058
- """Set the current position in the media."""
1059
- self._position = value
1060
-
1061
- @property
1062
- def last_position_update(self) -> Optional[int]:
1063
- """Return the last time the position was updated."""
1064
- return self._last_position_update
1065
-
1066
- @last_position_update.setter
1067
- def last_position_update(self, value: Optional[int]) -> None:
1068
- """Set the last position update time."""
1069
- self._last_position_update = value
1070
-
1071
- async def reset_progress(self) -> None:
1072
- """Reset the progress-related attributes."""
1073
- self.last_position_update = None
1074
- self.duration = None
1075
- self.position = None
1076
-
1077
- @property
1078
- def speed(self) -> Optional[int]:
1079
- """Return the speed."""
1080
- return self._speed
1081
-
1082
- @speed.setter
1083
- def speed(self, value: int | None) -> None:
1084
- """Set the channel ID."""
1085
- self._speed = value
1086
-
1087
- async def reset(self) -> None:
1088
- """Reset all playing information."""
1089
- self.channel_id = None
1090
- self.episode_number = None
1091
- self.season_number = None
1092
- self.episode_title = None
1093
- self.show_title = None
1094
- self.app_name = None
1095
- self.image = None
1096
- self.source_type = LGHorizonSourceType.UNKNOWN
1097
- self.speed = None
1098
- self.channel_name = None
1099
- self.id = None
1100
- self.start_time = None
1101
- self.end_time = None
1102
- await self.reset_progress()
1103
-
1104
-
1105
- class LGHorizonEntitlements:
1106
- """Class to represent entitlements."""
1107
-
1108
- def __init__(self, entitlements_json):
1109
- """Initialize entitlements."""
1110
- self.entitlements_json = entitlements_json
1111
-
1112
- @property
1113
- def entitlements(self):
1114
- """Returns the entitlements."""
1115
- return self.entitlements_json.get("entitlements", [])
1116
-
1117
- @property
1118
- def entitlement_ids(self) -> list[str]:
1119
- """Returns a list of entitlement IDs."""
1120
- return [e["id"] for e in self.entitlements if "id" in e]
1121
-
1122
-
1123
- class LGHorizonReplayEvent:
1124
- """LGhorizon replay event."""
1125
-
1126
- def __init__(self, raw_json: dict):
1127
- """Initialize an LG Horizon replay event."""
1128
- self._raw_json = raw_json
1129
-
1130
- @property
1131
- def episode_number(self) -> Optional[int]:
1132
- """Return the episode number."""
1133
- return self._raw_json.get("episodeNumber")
1134
-
1135
- @property
1136
- def channel_id(self) -> str:
1137
- """Return the channel ID."""
1138
- return self._raw_json["channelId"]
1139
-
1140
- @property
1141
- def event_id(self) -> str:
1142
- """Return the event ID."""
1143
- return self._raw_json["eventId"]
1144
-
1145
- @property
1146
- def season_number(self) -> Optional[int]:
1147
- """Return the season number."""
1148
- return self._raw_json.get("seasonNumber")
1149
-
1150
- @property
1151
- def start_time(self) -> Optional[int]:
1152
- """Return the season number."""
1153
- return self._raw_json.get("startTime", None)
1154
-
1155
- @property
1156
- def end_time(self) -> Optional[int]:
1157
- """Return the season number."""
1158
- return self._raw_json.get("endTime", None)
1159
-
1160
- @property
1161
- def title(self) -> str:
1162
- """Return the title of the event."""
1163
- return self._raw_json["title"]
1164
-
1165
- @property
1166
- def episode_name(self) -> Optional[str]:
1167
- """Return the episode name."""
1168
- return self._raw_json.get("episodeName", None)
1169
-
1170
- @property
1171
- def full_episode_title(self) -> Optional[str]:
1172
- """Return the full episode title."""
1173
-
1174
- if not self.season_number and not self.episode_number:
1175
- return None
1176
- full_title = f"""S{self.season_number:02d}E{self.episode_number:02d}"""
1177
- if self.episode_name:
1178
- full_title += f": {self.episode_name}"
1179
- return full_title
1180
-
1181
-
1182
- class LGHorizonVODType(Enum):
1183
- """Enumeration of LG Horizon VOD types."""
1184
-
1185
- ASSET = "ASSET"
1186
- EPISODE = "EPISODE"
1187
- UNKNOWN = "UNKNOWN"
1188
-
1189
-
1190
- class LGHorizonVOD:
1191
- """LGHorizon video on demand."""
1192
-
1193
- def __init__(self, vod_json) -> None:
1194
- """Initialize an LG Horizon VOD object.
1195
-
1196
- Args:
1197
- vod_json: The raw JSON dictionary containing VOD information.
1198
- """
1199
- self._vod_json = vod_json
1200
-
1201
- @property
1202
- def vod_type(self) -> LGHorizonVODType:
1203
- """Return the ID of the VOD."""
1204
- return LGHorizonVODType[self._vod_json.get("type", "unknown").upper()]
1205
-
1206
- @property
1207
- def id(self) -> str:
1208
- """Return the ID of the VOD."""
1209
- return self._vod_json["id"]
1210
-
1211
- @property
1212
- def season(self) -> Optional[int]:
1213
- """Return the season number of the recording."""
1214
- return self._vod_json.get("season", None)
1215
-
1216
- @property
1217
- def episode(self) -> Optional[int]:
1218
- """Return the episode number of the recording."""
1219
- return self._vod_json.get("episode", None)
1220
-
1221
- @property
1222
- def title(self) -> str:
1223
- """Return the ID of the VOD."""
1224
- return self._vod_json["title"]
1225
-
1226
- @property
1227
- def series_title(self) -> Optional[str]:
1228
- """Return the series title of the VOD."""
1229
- return self._vod_json.get("seriesTitle", None)
1230
-
1231
- @property
1232
- def duration(self) -> float:
1233
- """Return the duration of the VOD."""
1234
- return self._vod_json["duration"]
1235
-
1236
-
1237
- class LGHOrizonRelevantEpisode:
1238
- """Represents a relevant episode within a recording season or show."""
1239
-
1240
- def __init__(self, episode_json: dict) -> None:
1241
- """Abstract base class for LG Horizon recordings."""
1242
- self._episode_json = episode_json
1243
-
1244
- @property
1245
- def recording_state(self) -> LGHorizonRecordingState:
1246
- """Return the recording state."""
1247
- return LGHorizonRecordingState[
1248
- self._episode_json.get("recordingState", "unknown").upper()
1249
- ]
1250
-
1251
- @property
1252
- def season_number(self) -> Optional[int]:
1253
- """Return the season number of the recording."""
1254
- return self._episode_json.get("seasonNumber", None)
1255
-
1256
- @property
1257
- def episode_number(self) -> Optional[int]:
1258
- """Return the episode number of the recording."""
1259
- return self._episode_json.get("episodeNumber", None)
1260
-
1261
-
1262
- class LGHorizonRecording(ABC):
1263
- """Abstract base class for LG Horizon recordings."""
1264
-
1265
- @property
1266
- def recording_payload(self) -> dict:
1267
- """Return the payload of the message."""
1268
- return self._recording_payload
1269
-
1270
- @property
1271
- def recording_state(self) -> LGHorizonRecordingState:
1272
- """Return the recording state."""
1273
- return LGHorizonRecordingState[
1274
- self._recording_payload.get("recordingState", "unknown").upper()
1275
- ]
1276
-
1277
- @property
1278
- def source(self) -> LGHorizonRecordingSource:
1279
- """Return the recording source."""
1280
- return LGHorizonRecordingSource[
1281
- self._recording_payload.get("source", "unknown").upper()
1282
- ]
1283
-
1284
- @property
1285
- def type(self) -> LGHorizonRecordingType:
1286
- """Return the recording source."""
1287
- return LGHorizonRecordingType[
1288
- self._recording_payload.get("type", "unknown").upper()
1289
- ]
1290
-
1291
- @property
1292
- def id(self) -> str:
1293
- """Return the ID of the recording."""
1294
- return self._recording_payload["id"]
1295
-
1296
- @property
1297
- def title(self) -> str:
1298
- """Return the title of the recording."""
1299
- return self._recording_payload.get("title", "unknown")
1300
-
1301
- @property
1302
- def channel_id(self) -> str:
1303
- """Return the channel ID of the recording."""
1304
- return self._recording_payload["channelId"]
1305
-
1306
- @property
1307
- def poster_url(self) -> Optional[str]:
1308
- """Return the title of the recording."""
1309
- poster = self._recording_payload.get("poster")
1310
- if poster:
1311
- return poster.get("url")
1312
- return None
1313
-
1314
- def __init__(self, recording_payload: dict) -> None:
1315
- """Abstract base class for LG Horizon recordings.
1316
- Args:
1317
- recording_payload: The raw JSON dictionary containing recording information.
1318
- """
1319
- self._recording_payload = recording_payload
1320
-
1321
-
1322
- class LGHorizonRecordingSingle(LGHorizonRecording):
1323
- """LGHorizon recording."""
1324
-
1325
- @property
1326
- def episode_title(self) -> Optional[str]:
1327
- """Return the episode title of the recording."""
1328
- return self._recording_payload.get("episodeTitle", None)
1329
-
1330
- @property
1331
- def episode_id(self) -> Optional[str]:
1332
- """Return the episode title of the recording."""
1333
- return self._recording_payload.get("episodeId", None)
1334
-
1335
- @property
1336
- def season_number(self) -> Optional[int]:
1337
- """Return the season number of the recording."""
1338
- return self._recording_payload.get("seasonNumber", None)
1339
-
1340
- @property
1341
- def episode_number(self) -> Optional[int]:
1342
- """Return the episode number of the recording."""
1343
- return self._recording_payload.get("episodeNumber", None)
1344
-
1345
- @property
1346
- def show_id(self) -> Optional[str]:
1347
- """Return the show ID of the recording."""
1348
- return self._recording_payload.get("showId", None)
1349
-
1350
- @property
1351
- def show_title(self) -> Optional[str]:
1352
- """Return the show ID of the recording."""
1353
- return self._recording_payload.get("showTitle", None)
1354
-
1355
- @property
1356
- def season_id(self) -> Optional[str]:
1357
- """Return the season ID of the recording."""
1358
- return self._recording_payload.get("seasonId", None)
1359
-
1360
- @property
1361
- def channel_id(self) -> Optional[str]:
1362
- """Return the channel ID of the recording."""
1363
- return self._recording_payload.get("channelId", None)
1364
-
1365
- @property
1366
- def duration(self) -> Optional[int]:
1367
- """Return the title."""
1368
- return self.recording_payload.get("duration", None)
1369
-
1370
- @property
1371
- def start_time(self) -> Optional[int]:
1372
- """Return the title."""
1373
- return self.recording_payload.get("startTime", None)
1374
-
1375
- @property
1376
- def end_time(self) -> Optional[int]:
1377
- """Return the title."""
1378
- return self.recording_payload.get("endTime", None)
1379
-
1380
-
1381
- class LGHorizonRecordingSeason(LGHorizonRecording):
1382
- """Represents an LG Horizon recording season."""
1383
-
1384
- _most_relevant_epsode: Optional[LGHOrizonRelevantEpisode]
1385
-
1386
- def __init__(self, payload: dict) -> None:
1387
- """Abstract base class for LG Horizon recordings."""
1388
- super().__init__(payload)
1389
- episode_payload = payload.get("mostRelevantEpisode")
1390
- if episode_payload:
1391
- self._most_relevant_epsode = LGHOrizonRelevantEpisode(episode_payload)
1392
-
1393
- @property
1394
- def no_of_episodes(self) -> int:
1395
- """Return the number of episodes in the season."""
1396
- return self._recording_payload.get("noOfEpisodes", 0)
1397
-
1398
- @property
1399
- def season_title(self) -> str:
1400
- """Return the season title of the recording."""
1401
- return self._recording_payload.get("seasonTitle", "")
1402
-
1403
- @property
1404
- def show_id(self) -> str:
1405
- """Return the season title of the recording."""
1406
- return self._recording_payload.get("showId", "")
1407
-
1408
- @property
1409
- def most_relevant_episode(self) -> Optional[LGHOrizonRelevantEpisode]:
1410
- """Return the most relevant episode of the season."""
1411
- return self._most_relevant_epsode
1412
-
1413
-
1414
- class LGHorizonRecordingShow(LGHorizonRecording):
1415
- """Represents an LG Horizon recording show."""
1416
-
1417
- _most_relevant_epsode: Optional[LGHOrizonRelevantEpisode]
1418
-
1419
- def __init__(self, payload: dict) -> None:
1420
- """Abstract base class for LG Horizon recordings."""
1421
- super().__init__(payload)
1422
- episode_payload = payload.get("mostRelevantEpisode")
1423
- if episode_payload:
1424
- self._most_relevant_epsode = LGHOrizonRelevantEpisode(episode_payload)
1425
-
1426
- @property
1427
- def no_of_episodes(self) -> int:
1428
- """Return the number of episodes in the season."""
1429
- return self._recording_payload.get("noOfEpisodes", 0)
1430
-
1431
- @property
1432
- def most_relevant_episode(self) -> Optional[LGHOrizonRelevantEpisode]:
1433
- """Return the most relevant episode of the season."""
1434
- return self._most_relevant_epsode
1435
-
1436
-
1437
- class LGHorizonRecordingList:
1438
- """Represents a list of LG Horizon recordings."""
1439
-
1440
- @property
1441
- def total(self) -> int:
1442
- """Return the total number of recordings."""
1443
- return len(self._recordings)
1444
-
1445
- def __init__(self, recordings: List[LGHorizonRecording]) -> None:
1446
- """Initialize an LG Horizon recording list.
1447
-
1448
- Args:
1449
- recordings: A list of LGHorizonRecording objects.
1450
- """
1451
- self._recordings = recordings
1452
-
1453
- @property
1454
- def recordings(self) -> List[LGHorizonRecording]:
1455
- """Return the total number of recordings."""
1456
- return self._recordings
1457
-
1458
-
1459
- class LGHorizonShowRecordingList(LGHorizonRecordingList):
1460
- """LGHorizon recording."""
1461
-
1462
- def __init__(
1463
- self,
1464
- show_title: Optional[str],
1465
- show_image,
1466
- recordings: List[LGHorizonRecording],
1467
- ) -> None:
1468
- """Initialize an LG Horizon show recording list.
1469
-
1470
- Args:
1471
- show_title: The title of the show.
1472
- show_image: The image URL for the show.
1473
- recordings: A list of LGHorizonRecording objects belonging to the show.
1474
- """
1475
- super().__init__(recordings)
1476
- self._show_title = show_title
1477
- self._show_image = show_image
1478
-
1479
- @property
1480
- def show_title(self) -> str:
1481
- """Title of the show."""
1482
- return self._show_title
1483
-
1484
- @property
1485
- def show_image(self) -> Optional[str]:
1486
- """Image of the show."""
1487
- return self._show_image
1488
-
1489
-
1490
- class LGHorizonRecordingQuota:
1491
- """LGHorizon recording quota."""
1492
-
1493
- def __init__(self, quota_json: dict) -> None:
1494
- """Initialize the recording quota."""
1495
- self._quota_json = quota_json
1496
-
1497
- @property
1498
- def quota(self) -> int:
1499
- """Return the total space in MB."""
1500
- return self._quota_json.get("quota", 0)
1501
-
1502
- @property
1503
- def occupied(self) -> int:
1504
- """Return the used space in MB."""
1505
- return self._quota_json.get("occupied", 0)
1506
-
1507
- @property
1508
- def percentage_used(self) -> float:
1509
- """Return the percentage of space used."""
1510
- if self.quota == 0:
1511
- return 0.0
1512
- return (self.occupied / self.quota) * 100