lghorizon 0.9.0b0__tar.gz → 0.9.0.dev1__tar.gz

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