lghorizon 0.8.7__py3-none-any.whl → 0.9.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lghorizon/__init__.py +3 -38
- lghorizon/const.py +11 -4
- lghorizon/helpers.py +1 -1
- lghorizon/lghorizon_api.py +237 -506
- lghorizon/lghorizon_device.py +336 -0
- lghorizon/lghorizon_device_state_processor.py +301 -0
- lghorizon/lghorizon_message_factory.py +39 -0
- lghorizon/lghorizon_models.py +1331 -0
- lghorizon/lghorizon_mqtt_client.py +123 -0
- lghorizon/lghorizon_recording_factory.py +41 -0
- {lghorizon-0.8.7.dist-info → lghorizon-0.9.0b0.dist-info}/METADATA +1 -1
- lghorizon-0.9.0b0.dist-info/RECORD +17 -0
- {lghorizon-0.8.7.dist-info → lghorizon-0.9.0b0.dist-info}/WHEEL +1 -1
- lghorizon/models.py +0 -768
- lghorizon-0.8.7.dist-info/RECORD +0 -12
- {lghorizon-0.8.7.dist-info → lghorizon-0.9.0b0.dist-info}/licenses/LICENSE +0 -0
- {lghorizon-0.8.7.dist-info → lghorizon-0.9.0b0.dist-info}/top_level.txt +0 -0
lghorizon/lghorizon_api.py
CHANGED
|
@@ -1,539 +1,270 @@
|
|
|
1
|
-
"""
|
|
2
|
-
# pylint: disable=broad-exception-caught
|
|
3
|
-
# pylint: disable=line-too-long
|
|
1
|
+
"""LG Horizon API client."""
|
|
4
2
|
|
|
5
3
|
import logging
|
|
6
|
-
import
|
|
7
|
-
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,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
_logger = logging.getLogger(__name__)
|
|
50
|
-
_supported_platforms = ["EOS", "EOS2", "HORIZON", "APOLLO"]
|
|
4
|
+
from typing import Any, Dict, cast
|
|
51
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
|
|
52
21
|
|
|
53
|
-
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
|
|
92
|
-
self._profile_id = profile_id
|
|
93
22
|
|
|
94
|
-
|
|
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"
|
|
133
|
-
)
|
|
134
|
-
headers = {"content-type": "application/json", "charset": "utf-8"}
|
|
135
|
-
payload = '{"refreshToken":"' + self.refresh_token + '"}'
|
|
23
|
+
_LOGGER = logging.getLogger(__name__)
|
|
136
24
|
|
|
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"]
|
|
185
25
|
|
|
186
|
-
|
|
187
|
-
|
|
26
|
+
class LGHorizonApi:
|
|
27
|
+
"""LG Horizon API client."""
|
|
28
|
+
|
|
29
|
+
_mqtt_client: LGHorizonMqttClient
|
|
30
|
+
auth: LGHorizonAuth
|
|
31
|
+
_service_config: LGHorizonServicesConfig
|
|
32
|
+
_customer: LGHorizonCustomer
|
|
33
|
+
_channels: Dict[str, LGHorizonChannel]
|
|
34
|
+
_entitlements: LGHorizonEntitlements
|
|
35
|
+
_profile_id: str
|
|
36
|
+
_initialized: bool = False
|
|
37
|
+
_devices: Dict[str, LGHorizonDevice] = {}
|
|
38
|
+
_message_factory: LGHorizonMessageFactory = LGHorizonMessageFactory()
|
|
39
|
+
_device_state_processor: LGHorizonDeviceStateProcessor | None
|
|
40
|
+
_recording_factory: LGHorizonRecordingFactory = LGHorizonRecordingFactory()
|
|
41
|
+
|
|
42
|
+
def __init__(self, auth: LGHorizonAuth, profile_id: str = "") -> None:
|
|
43
|
+
"""Initialize LG Horizon API client."""
|
|
44
|
+
self.auth = auth
|
|
45
|
+
self._profile_id = profile_id
|
|
46
|
+
self._channels = {}
|
|
47
|
+
self._device_state_processor = None
|
|
48
|
+
|
|
49
|
+
async def initialize(self) -> None:
|
|
50
|
+
"""Initialize the API client."""
|
|
51
|
+
self._service_config = await self.auth.get_service_config()
|
|
52
|
+
self._customer = await self._get_customer_info()
|
|
53
|
+
if self._profile_id == "":
|
|
54
|
+
self._profile_id = list(self._customer.profiles.keys())[0]
|
|
55
|
+
await self._refresh_entitlements()
|
|
56
|
+
await self._refresh_channels()
|
|
57
|
+
self._mqtt_client = await self._create_mqtt_client()
|
|
58
|
+
await self._mqtt_client.connect()
|
|
59
|
+
await self._register_devices()
|
|
60
|
+
self._device_state_processor = LGHorizonDeviceStateProcessor(
|
|
61
|
+
self.auth, self._channels, self._customer, self._profile_id
|
|
62
|
+
)
|
|
63
|
+
self._initialized = True
|
|
188
64
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
65
|
+
async def get_devices(self) -> Dict[str, LGHorizonDevice]:
|
|
66
|
+
"""Get devices."""
|
|
67
|
+
if not self._initialized:
|
|
68
|
+
raise RuntimeError("LGHorizonApi not initialized")
|
|
192
69
|
|
|
193
|
-
|
|
70
|
+
return self._devices
|
|
194
71
|
|
|
195
|
-
|
|
196
|
-
|
|
72
|
+
async def get_profiles(self) -> Dict[str, LGHorizonProfile]:
|
|
73
|
+
"""Get profile IDs."""
|
|
74
|
+
if not self._initialized:
|
|
75
|
+
raise RuntimeError("LGHorizonApi not initialized")
|
|
197
76
|
|
|
198
|
-
|
|
199
|
-
username_fieldname: self.username,
|
|
200
|
-
pasword_fieldname: self.password,
|
|
201
|
-
"rememberme": "true",
|
|
202
|
-
}
|
|
77
|
+
return self._customer.profiles
|
|
203
78
|
|
|
204
|
-
|
|
205
|
-
|
|
79
|
+
async def get_profile_channels(
|
|
80
|
+
self, profile_id: str
|
|
81
|
+
) -> Dict[str, LGHorizonChannel]:
|
|
82
|
+
"""Returns channels to display baed on profile."""
|
|
83
|
+
# Attempt to retrieve the profile by the given profile_id
|
|
84
|
+
profile = self._customer.profiles.get(profile_id)
|
|
85
|
+
|
|
86
|
+
# If the specified profile is not found, and there are other profiles available,
|
|
87
|
+
# default to the first profile in the customer's list.
|
|
88
|
+
if not profile and self._customer.profiles:
|
|
89
|
+
_LOGGER.debug(
|
|
90
|
+
"Profile with ID '%s' not found. Defaulting to first available profile.",
|
|
91
|
+
profile_id,
|
|
206
92
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
]
|
|
219
|
-
code_matches = re.findall(r"code=(.*)&", success_url)
|
|
220
|
-
|
|
221
|
-
authorization_code = code_matches[0]
|
|
222
|
-
|
|
223
|
-
new_payload = {
|
|
224
|
-
"authorizationGrant": {
|
|
225
|
-
"authorizationCode": authorization_code,
|
|
226
|
-
"validityToken": authorization_validity_token,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
headers = {
|
|
230
|
-
"content-type": "application/json",
|
|
93
|
+
profile = list(self._customer.profiles.values())[0]
|
|
94
|
+
|
|
95
|
+
# If a profile is found and it has favorite channels, filter the main channels list.
|
|
96
|
+
if profile and profile.favorite_channels:
|
|
97
|
+
_LOGGER.debug("Returning favorite channels for profile '%s'.", profile.name)
|
|
98
|
+
# Use a set for faster lookup of favorite channel IDs
|
|
99
|
+
profile_channel_ids = set(profile.favorite_channels)
|
|
100
|
+
return {
|
|
101
|
+
channel.id: channel
|
|
102
|
+
for channel in self._channels.values()
|
|
103
|
+
if channel.id in profile_channel_ids
|
|
231
104
|
}
|
|
232
|
-
|
|
233
|
-
|
|
105
|
+
|
|
106
|
+
# If no profile is found (even after defaulting) or the profile has no favorite channels,
|
|
107
|
+
# return all available channels.
|
|
108
|
+
_LOGGER.debug("No specific profile channels found, returning all channels.")
|
|
109
|
+
return self._channels
|
|
110
|
+
|
|
111
|
+
async def _register_devices(self) -> None:
|
|
112
|
+
"""Register devices."""
|
|
113
|
+
_LOGGER.debug("Registering devices...")
|
|
114
|
+
self._devices = {}
|
|
115
|
+
channels = await self.get_profile_channels(self._profile_id)
|
|
116
|
+
for raw_box in self._customer.assigned_devices:
|
|
117
|
+
_LOGGER.debug("Creating box for device: %s", raw_box)
|
|
118
|
+
if self._device_state_processor is None:
|
|
119
|
+
self._device_state_processor = LGHorizonDeviceStateProcessor(
|
|
120
|
+
self.auth, self._channels, self._customer, self._profile_id
|
|
121
|
+
)
|
|
122
|
+
device = LGHorizonDevice(
|
|
123
|
+
raw_box,
|
|
124
|
+
self._mqtt_client,
|
|
125
|
+
self._device_state_processor,
|
|
126
|
+
self.auth,
|
|
127
|
+
channels,
|
|
234
128
|
)
|
|
235
|
-
self.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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"],
|
|
129
|
+
self._devices[device.device_id] = device
|
|
130
|
+
|
|
131
|
+
async def disconnect(self) -> None:
|
|
132
|
+
"""Disconnect the client."""
|
|
133
|
+
if self._mqtt_client:
|
|
134
|
+
await self._mqtt_client.disconnect()
|
|
135
|
+
self._initialized = False
|
|
136
|
+
|
|
137
|
+
async def _create_mqtt_client(self) -> LGHorizonMqttClient:
|
|
138
|
+
mqtt_client = await LGHorizonMqttClient.create(
|
|
139
|
+
self.auth,
|
|
266
140
|
self._on_mqtt_connected,
|
|
267
141
|
self._on_mqtt_message,
|
|
268
142
|
)
|
|
143
|
+
return mqtt_client
|
|
144
|
+
|
|
145
|
+
async def _on_mqtt_connected(self):
|
|
146
|
+
"""MQTT connected callback."""
|
|
147
|
+
await self._mqtt_client.subscribe(self.auth.household_id)
|
|
148
|
+
# await self._mqtt_client.subscribe(self.auth.household_id + "/#")
|
|
149
|
+
await self._mqtt_client.subscribe(
|
|
150
|
+
self.auth.household_id + "/" + self._mqtt_client.client_id
|
|
151
|
+
)
|
|
152
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/+/status")
|
|
153
|
+
await self._mqtt_client.subscribe(
|
|
154
|
+
self.auth.household_id + "/+/networkRecordings"
|
|
155
|
+
)
|
|
156
|
+
await self._mqtt_client.subscribe(
|
|
157
|
+
self.auth.household_id + "/+/networkRecordings/capacity"
|
|
158
|
+
)
|
|
159
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/+/localRecordings")
|
|
160
|
+
await self._mqtt_client.subscribe(
|
|
161
|
+
self.auth.household_id + "/+/localRecordings/capacity"
|
|
162
|
+
)
|
|
163
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/watchlistService")
|
|
164
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/purchaseService")
|
|
165
|
+
await self._mqtt_client.subscribe(
|
|
166
|
+
self.auth.household_id + "/personalizationService"
|
|
167
|
+
)
|
|
168
|
+
await self._mqtt_client.subscribe(self.auth.household_id + "/recordingStatus")
|
|
169
|
+
await self._mqtt_client.subscribe(
|
|
170
|
+
self.auth.household_id + "/recordingStatus/lastUserAction"
|
|
171
|
+
)
|
|
269
172
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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"
|
|
173
|
+
async def _on_mqtt_message(self, mqtt_message: dict, mqtt_topic: str):
|
|
174
|
+
"""MQTT message callback."""
|
|
175
|
+
message = await self._message_factory.create_message(mqtt_topic, mqtt_message)
|
|
176
|
+
match message.message_type:
|
|
177
|
+
case LGHorizonMessageType.STATUS:
|
|
178
|
+
message.__class__ = LGHorizonStatusMessage
|
|
179
|
+
status_message = cast(LGHorizonStatusMessage, message)
|
|
180
|
+
device = self._devices[status_message.source]
|
|
181
|
+
await device.handle_status_message(status_message)
|
|
182
|
+
case LGHorizonMessageType.UI_STATUS:
|
|
183
|
+
message.__class__ = LGHorizonUIStatusMessage
|
|
184
|
+
ui_status_message = cast(LGHorizonUIStatusMessage, message)
|
|
185
|
+
device = self._devices[ui_status_message.source]
|
|
186
|
+
if (
|
|
187
|
+
not device.device_state.state
|
|
188
|
+
== LGHorizonRunningState.ONLINE_RUNNING
|
|
189
|
+
):
|
|
190
|
+
return
|
|
191
|
+
await device.handle_ui_status_message(ui_status_message)
|
|
192
|
+
|
|
193
|
+
async def _get_customer_info(self) -> LGHorizonCustomer:
|
|
194
|
+
service_url = await self._service_config.get_service_url(
|
|
195
|
+
"personalizationService"
|
|
428
196
|
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def _get_channels(self):
|
|
433
|
-
self._update_entitlements()
|
|
434
|
-
_logger.info("Retrieving channels...")
|
|
435
|
-
channels_result = self._do_api_call(
|
|
436
|
-
f"{self._config['linearService']['URL']}/v2/channels?cityId={self.customer.city_id}&language={self._country_settings['language']}&productClass=Orion-DASH"
|
|
197
|
+
result = await self.auth.request(
|
|
198
|
+
service_url,
|
|
199
|
+
f"/v1/customer/{self.auth.household_id}?with=profiles%2Cdevices",
|
|
437
200
|
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
201
|
+
return LGHorizonCustomer(result)
|
|
202
|
+
|
|
203
|
+
async def _refresh_entitlements(self) -> Any:
|
|
204
|
+
"""Retrieve entitlements."""
|
|
205
|
+
_LOGGER.debug("Retrieving entitlements...")
|
|
206
|
+
service_url = await self._service_config.get_service_url("purchaseService")
|
|
207
|
+
result = await self.auth.request(
|
|
208
|
+
service_url,
|
|
209
|
+
f"/v2/customers/{self.auth.household_id}/entitlements?enableDaypass=true",
|
|
210
|
+
)
|
|
211
|
+
self._entitlements = LGHorizonEntitlements(result)
|
|
212
|
+
|
|
213
|
+
async def _refresh_channels(self):
|
|
214
|
+
"""Retrieve channels."""
|
|
215
|
+
_LOGGER.debug("Retrieving channels...")
|
|
216
|
+
service_url = await self._service_config.get_service_url("linearService")
|
|
217
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
218
|
+
channels_json = await self.auth.request(
|
|
219
|
+
service_url,
|
|
220
|
+
f"/v2/channels?cityId={self._customer.city_id}&language={lang}&productClass=Orion-DASH",
|
|
221
|
+
)
|
|
222
|
+
for channel_json in channels_json:
|
|
223
|
+
channel = LGHorizonChannel(channel_json)
|
|
441
224
|
common_entitlements = list(
|
|
442
|
-
set(self._entitlements) & set(channel
|
|
225
|
+
set(self._entitlements.entitlement_ids) & set(channel.linear_products)
|
|
443
226
|
)
|
|
227
|
+
|
|
444
228
|
if len(common_entitlements) == 0:
|
|
445
229
|
continue
|
|
446
|
-
channel_id = channel["id"]
|
|
447
|
-
self._channels[channel_id] = LGHorizonChannel(channel)
|
|
448
|
-
_logger.info("%s retrieved.", len(self._channels))
|
|
449
230
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
channel for channel in all_channels if channel.id in profile_channel_ids
|
|
461
|
-
]
|
|
462
|
-
|
|
463
|
-
def _get_replay_event(self, listing_id) -> Any:
|
|
464
|
-
"""Get listing."""
|
|
465
|
-
_logger.info("Retrieving replay event details...")
|
|
466
|
-
response = self._do_api_call(
|
|
467
|
-
f"{self._config['linearService']['URL']}/v2/replayEvent/{listing_id}?returnLinearContent=true&language={self._country_settings['language']}"
|
|
468
|
-
)
|
|
469
|
-
_logger.info("Replay event details retrieved")
|
|
470
|
-
return response
|
|
471
|
-
|
|
472
|
-
def get_recording_capacity(self) -> int:
|
|
473
|
-
"""Returns remaining recording capacity"""
|
|
474
|
-
ctry_code = self._country_code[0:2]
|
|
475
|
-
if ctry_code == "gb":
|
|
476
|
-
_logger.debug("GB: not supported")
|
|
477
|
-
return None
|
|
478
|
-
try:
|
|
479
|
-
_logger.info("Retrieving recordingcapacity...")
|
|
480
|
-
quota_content = self._do_api_call(
|
|
481
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/quota"
|
|
482
|
-
)
|
|
483
|
-
if "quota" not in quota_content and "occupied" not in quota_content:
|
|
484
|
-
_logger.error("Unable to fetch recording capacity...")
|
|
485
|
-
return None
|
|
486
|
-
capacity = (quota_content["occupied"] / quota_content["quota"]) * 100
|
|
487
|
-
self.recording_capacity = round(capacity)
|
|
488
|
-
_logger.debug("Remaining recordingcapacity %s %%", self.recording_capacity)
|
|
489
|
-
return self.recording_capacity
|
|
490
|
-
except Exception:
|
|
491
|
-
_logger.error("Unable to fetch recording capacity...")
|
|
492
|
-
return None
|
|
493
|
-
|
|
494
|
-
def get_recordings(self) -> List[LGHorizonBaseRecording]:
|
|
495
|
-
"""Returns recordings."""
|
|
496
|
-
_logger.info("Retrieving recordings...")
|
|
497
|
-
recording_content = self._do_api_call(
|
|
498
|
-
f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/recordings?sort=time&sortOrder=desc&language={self._country_settings['language']}"
|
|
231
|
+
self._channels[channel.id] = channel
|
|
232
|
+
|
|
233
|
+
async def get_all_recordings(self) -> LGHorizonRecordingList:
|
|
234
|
+
"""Retrieve all recordings."""
|
|
235
|
+
_LOGGER.debug("Retrieving recordings...")
|
|
236
|
+
service_url = await self._service_config.get_service_url("recordingService")
|
|
237
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
238
|
+
recordings_json = await self.auth.request(
|
|
239
|
+
service_url,
|
|
240
|
+
f"/customers/{self.auth.household_id}/recordings?isAdult=false&offset=0&limit=100&sort=time&sortOrder=desc&profileId={self._profile_id}&language={lang}",
|
|
499
241
|
)
|
|
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))
|
|
242
|
+
recordings = await self._recording_factory.create_recordings(recordings_json)
|
|
508
243
|
return recordings
|
|
509
244
|
|
|
510
|
-
def
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
245
|
+
async def get_show_recordings(
|
|
246
|
+
self, show_id: str, channel_id: str
|
|
247
|
+
) -> LGHorizonRecordingList:
|
|
248
|
+
"""Retrieve all recordings."""
|
|
249
|
+
_LOGGER.debug("Retrieving recordings fro show...")
|
|
250
|
+
service_url = await self._service_config.get_service_url("recordingService")
|
|
251
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
252
|
+
episodes_json = await self.auth.request(
|
|
253
|
+
service_url,
|
|
254
|
+
f"/customers/8436830_nl/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
255
|
)
|
|
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))
|
|
256
|
+
recordings = await self._recording_factory.create_episodes(episodes_json)
|
|
523
257
|
return recordings
|
|
524
258
|
|
|
525
|
-
def
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
259
|
+
async def get_recording_quota(self) -> LGHorizonRecordingQuota:
|
|
260
|
+
"""Refresh recording quota."""
|
|
261
|
+
_LOGGER.debug("Refreshing recording quota...")
|
|
262
|
+
service_url = await self._service_config.get_service_url("recordingService")
|
|
263
|
+
quota_json = await self.auth.request(
|
|
264
|
+
service_url,
|
|
265
|
+
f"/customers/{self.auth.household_id}/quota",
|
|
529
266
|
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def _get_config(self, country_code: str):
|
|
535
|
-
base_country_code = country_code[0:2]
|
|
536
|
-
config_url = f"{self._country_settings['api_url']}/{base_country_code}/en/config-service/conf/web/backoffice.json"
|
|
537
|
-
result = self._do_api_call(config_url)
|
|
538
|
-
_logger.debug(result)
|
|
539
|
-
return result
|
|
267
|
+
return LGHorizonRecordingQuota(quota_json)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
__all__ = ["LGHorizonApi", "LGHorizonAuth"]
|