lghorizon 0.9.0.dev4__py3-none-any.whl → 0.9.2__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.
@@ -0,0 +1,364 @@
1
+ """LG Horizon device (set-top box) model."""
2
+
3
+ import random
4
+ import json
5
+ import urllib.parse
6
+ from datetime import datetime as dt, timezone
7
+
8
+ import time
9
+
10
+ from typing import cast, Dict, Optional
11
+
12
+ from .lghorizon_models import LGHorizonDeviceState, LGHorizonRunningState
13
+ from .lghorizon_models import LGHorizonStatusMessage, LGHorizonUIStatusMessage
14
+ from .lghorizon_models import (
15
+ LGHorizonSourceType,
16
+ LGHorizonLinearSource,
17
+ LGHorizonVODSource,
18
+ LGHorizonReplaySource,
19
+ LGHorizonNDVRSource,
20
+ LGHorizonReviewBufferSource,
21
+ LGHorizonRecordingSource,
22
+ )
23
+ from .lghorizon_models import LGHorizonAuth
24
+ from .lghorizon_models import LGHorizonReplayEvent, LGHorizonVOD, LGHorizonVODType
25
+
26
+ from .lghorizon_models import LGHorizonRecordingSingle
27
+ from .lghorizon_models import LGHorizonChannel
28
+ from .lghorizon_models import (
29
+ LGHorizonUIStateType,
30
+ LGHorizonAppsState,
31
+ LGHorizonPlayerState,
32
+ )
33
+ from .lghorizon_models import LGHorizonCustomer
34
+
35
+
36
+ class LGHorizonDeviceStateProcessor:
37
+ """Process incoming device state messages"""
38
+
39
+ def __init__(
40
+ self,
41
+ auth: LGHorizonAuth,
42
+ channels: Dict[str, LGHorizonChannel],
43
+ customer: LGHorizonCustomer,
44
+ profile_id: str,
45
+ ):
46
+ self._auth = auth
47
+ self._channels = channels
48
+ self._customer = customer
49
+ self._profile_id = profile_id
50
+
51
+ async def process_state(
52
+ self, device_state: LGHorizonDeviceState, status_message: LGHorizonStatusMessage
53
+ ) -> None:
54
+ """Process the device state based on the status message."""
55
+ await device_state.reset()
56
+ device_state.state = status_message.running_state
57
+
58
+ async def process_ui_state(
59
+ self,
60
+ device_state: LGHorizonDeviceState,
61
+ ui_status_message: LGHorizonUIStatusMessage,
62
+ ) -> None:
63
+ """Process the device state based on the UI status message."""
64
+ await device_state.reset()
65
+ if (
66
+ ui_status_message.ui_state is None
67
+ or device_state.state == LGHorizonRunningState.ONLINE_STANDBY
68
+ ):
69
+ await device_state.reset()
70
+ return
71
+
72
+ if ui_status_message.ui_state is None:
73
+ return
74
+ match ui_status_message.ui_state.ui_status:
75
+ case LGHorizonUIStateType.MAINUI:
76
+ if ui_status_message.ui_state.player_state is None:
77
+ return
78
+ await self._process_main_ui_state(
79
+ device_state, ui_status_message.ui_state.player_state
80
+ )
81
+ case LGHorizonUIStateType.APPS:
82
+ if ui_status_message.ui_state.apps_state is None:
83
+ return
84
+ await self._process_apps_state(
85
+ device_state, ui_status_message.ui_state.apps_state
86
+ )
87
+
88
+ if ui_status_message.ui_state.ui_status == LGHorizonUIStateType.APPS:
89
+ return
90
+
91
+ if ui_status_message.ui_state.player_state is None:
92
+ return
93
+
94
+ async def _process_main_ui_state(
95
+ self,
96
+ device_state: LGHorizonDeviceState,
97
+ player_state: LGHorizonPlayerState,
98
+ ) -> None:
99
+ if player_state is None:
100
+ return
101
+ await device_state.reset()
102
+ device_state.source_type = player_state.source_type
103
+ device_state.ui_state_type = LGHorizonUIStateType.MAINUI
104
+ device_state.speed = player_state.speed
105
+
106
+ match player_state.source_type:
107
+ case LGHorizonSourceType.LINEAR:
108
+ await self._process_linear_state(device_state, player_state)
109
+ case LGHorizonSourceType.REVIEWBUFFER:
110
+ await self._process_reviewbuffer_state(device_state, player_state)
111
+ case LGHorizonSourceType.REPLAY:
112
+ await self._process_replay_state(device_state, player_state)
113
+ case LGHorizonSourceType.VOD:
114
+ await self._process_vod_state(device_state, player_state)
115
+ case LGHorizonSourceType.NDVR:
116
+ await self._process_ndvr_state(device_state, player_state)
117
+
118
+ async def _process_apps_state(
119
+ self,
120
+ device_state: LGHorizonDeviceState,
121
+ apps_state: LGHorizonAppsState,
122
+ ) -> None:
123
+ device_state.id = apps_state.id
124
+ device_state.show_title = apps_state.app_name
125
+ device_state.image = apps_state.logo_path
126
+ device_state.ui_state_type = LGHorizonUIStateType.APPS
127
+
128
+ async def _process_linear_state(
129
+ self,
130
+ device_state: LGHorizonDeviceState,
131
+ player_state: LGHorizonPlayerState,
132
+ ) -> None:
133
+ """Process the device state based on the UI status message."""
134
+ if player_state.source is None:
135
+ return
136
+ player_state.source.__class__ = LGHorizonLinearSource
137
+ source = cast(LGHorizonLinearSource, player_state.source)
138
+ service_config = await self._auth.get_service_config()
139
+ service_url = await service_config.get_service_url("linearService")
140
+ lang = await self._customer.get_profile_lang(self._profile_id)
141
+ service_path = f"/v2/replayEvent/{source.event_id}?returnLinearContent=true&language={lang}"
142
+
143
+ event_json = await self._auth.request(
144
+ service_url,
145
+ service_path,
146
+ )
147
+ replay_event = LGHorizonReplayEvent(event_json)
148
+ device_state.id = replay_event.event_id
149
+ channel = self._channels[replay_event.channel_id]
150
+ device_state.source_type = source.source_type
151
+ device_state.channel_id = channel.id
152
+ device_state.channel_name = channel.title
153
+ device_state.episode_title = replay_event.episode_name
154
+ device_state.season_number = replay_event.season_number
155
+ device_state.episode_number = replay_event.episode_number
156
+ device_state.show_title = replay_event.title
157
+ device_state.last_position_update = int(time.time())
158
+ device_state.start_time = replay_event.start_time
159
+ device_state.end_time = replay_event.end_time
160
+ device_state.duration = replay_event.end_time - replay_event.start_time
161
+ device_state.position = int(time.time()) - int(replay_event.start_time)
162
+
163
+ # Add random number to url to force refresh
164
+ join_param = "?"
165
+ if join_param in channel.stream_image:
166
+ join_param = "&"
167
+ image_url = (
168
+ f"{channel.stream_image}{join_param}{str(random.randrange(1000000))}"
169
+ )
170
+ device_state.image = image_url
171
+
172
+ async def _process_reviewbuffer_state(
173
+ self,
174
+ device_state: LGHorizonDeviceState,
175
+ player_state: LGHorizonPlayerState,
176
+ ) -> None:
177
+ """Process the device state based on the UI status message."""
178
+ if player_state.source is None:
179
+ return
180
+ player_state.source.__class__ = LGHorizonReviewBufferSource
181
+ source = cast(LGHorizonReviewBufferSource, player_state.source)
182
+ service_config = await self._auth.get_service_config()
183
+ service_url = await service_config.get_service_url("linearService")
184
+ lang = await self._customer.get_profile_lang(self._profile_id)
185
+ service_path = f"/v2/replayEvent/{source.event_id}?returnLinearContent=true&language={lang}"
186
+
187
+ event_json = await self._auth.request(
188
+ service_url,
189
+ service_path,
190
+ )
191
+ replay_event = LGHorizonReplayEvent(event_json)
192
+ device_state.id = replay_event.event_id
193
+ channel = self._channels[replay_event.channel_id]
194
+ device_state.source_type = source.source_type
195
+ device_state.channel_id = channel.id
196
+ device_state.channel_name = channel.title
197
+ device_state.episode_title = replay_event.episode_name
198
+ device_state.season_number = replay_event.season_number
199
+ device_state.episode_number = replay_event.episode_number
200
+ device_state.show_title = replay_event.title
201
+ device_state.last_position_update = int(
202
+ player_state.last_speed_change_time / 1000
203
+ )
204
+ device_state.position = int(player_state.relative_position / 1000)
205
+ device_state.start_time = replay_event.start_time
206
+ device_state.end_time = replay_event.end_time
207
+ device_state.duration = replay_event.end_time - replay_event.start_time
208
+ # Add random number to url to force refresh
209
+ join_param = "?"
210
+ if join_param in channel.stream_image:
211
+ join_param = "&"
212
+ image_url = (
213
+ f"{channel.stream_image}{join_param}{str(random.randrange(1000000))}"
214
+ )
215
+ device_state.image = image_url
216
+
217
+ async def _process_replay_state(
218
+ self,
219
+ device_state: LGHorizonDeviceState,
220
+ player_state: LGHorizonPlayerState,
221
+ ) -> None:
222
+ """Process the device state based on the UI status message."""
223
+ if player_state.source is None:
224
+ return
225
+ player_state.source.__class__ = LGHorizonReplaySource
226
+ source = cast(LGHorizonReplaySource, player_state.source)
227
+ service_config = await self._auth.get_service_config()
228
+ service_url = await service_config.get_service_url("linearService")
229
+ lang = await self._customer.get_profile_lang(self._profile_id)
230
+ service_path = f"/v2/replayEvent/{source.event_id}?returnLinearContent=true&language={lang}"
231
+
232
+ event_json = await self._auth.request(
233
+ service_url,
234
+ service_path,
235
+ )
236
+ replay_event = LGHorizonReplayEvent(event_json)
237
+ device_state.id = replay_event.event_id
238
+ # Iets met buffer doen
239
+ channel = self._channels[replay_event.channel_id]
240
+ device_state.source_type = source.source_type
241
+ device_state.channel_id = channel.id
242
+ device_state.episode_title = replay_event.episode_name
243
+ device_state.season_number = replay_event.season_number
244
+ device_state.episode_number = replay_event.episode_number
245
+ device_state.show_title = replay_event.title
246
+ device_state.last_position_update = int(
247
+ player_state.last_speed_change_time / 1000
248
+ )
249
+ device_state.start_time = replay_event.start_time
250
+ device_state.end_time = replay_event.end_time
251
+ device_state.duration = replay_event.end_time - replay_event.start_time
252
+ device_state.position = int(player_state.relative_position / 1000)
253
+ # Add random number to url to force refresh
254
+ device_state.image = await self._get_intent_image_url(replay_event.event_id)
255
+
256
+ async def _process_vod_state(
257
+ self,
258
+ device_state: LGHorizonDeviceState,
259
+ player_state: LGHorizonPlayerState,
260
+ ) -> None:
261
+ """Process the device state based on the UI status message."""
262
+ if player_state.source is None:
263
+ return
264
+ player_state.source.__class__ = LGHorizonVODSource
265
+ source = cast(LGHorizonVODSource, player_state.source)
266
+ service_config = await self._auth.get_service_config()
267
+ service_url = await service_config.get_service_url("vodService")
268
+ lang = await self._customer.get_profile_lang(self._profile_id)
269
+ service_path = f"/v2/detailscreen/{source.title_id}?language={lang}&profileId={self._profile_id}&cityId={self._customer.city_id}"
270
+
271
+ vod_json = await self._auth.request(
272
+ service_url,
273
+ service_path,
274
+ )
275
+ vod = LGHorizonVOD(vod_json)
276
+ device_state.id = vod.id
277
+ if vod.vod_type == LGHorizonVODType.EPISODE:
278
+ device_state.show_title = vod.series_title
279
+ device_state.episode_title = vod.title
280
+ device_state.season_number = vod.season
281
+ device_state.episode_number = vod.episode
282
+ else:
283
+ device_state.show_title = vod.title
284
+
285
+ device_state.duration = vod.duration
286
+ device_state.last_position_update = int(time.time())
287
+ device_state.position = int(player_state.relative_position / 1000)
288
+
289
+ device_state.image = await self._get_intent_image_url(vod.id)
290
+
291
+ async def _process_ndvr_state(
292
+ self, device_state: LGHorizonDeviceState, player_state: LGHorizonPlayerState
293
+ ) -> None:
294
+ """Process the device state based on the UI status message."""
295
+ if player_state.source is None:
296
+ return
297
+ player_state.source.__class__ = LGHorizonNDVRSource
298
+ source = cast(LGHorizonNDVRSource, player_state.source)
299
+ service_config = await self._auth.get_service_config()
300
+ service_url = await service_config.get_service_url("recordingService")
301
+ lang = await self._customer.get_profile_lang(self._profile_id)
302
+ service_path = f"/customers/{self._customer.customer_id}/details/single/{source.recording_id}?profileId={self._profile_id}&language={lang}"
303
+ recording_json = await self._auth.request(
304
+ service_url,
305
+ service_path,
306
+ )
307
+ recording = LGHorizonRecordingSingle(recording_json)
308
+ device_state.id = recording.id
309
+ device_state.channel_id = recording.channel_id
310
+ if recording.channel_id:
311
+ channel = self._channels[recording.channel_id]
312
+ device_state.channel_name = channel.title
313
+
314
+ device_state.episode_title = recording.episode_title
315
+ device_state.season_number = recording.season_number
316
+ device_state.episode_number = recording.episode_number
317
+ device_state.last_position_update = int(
318
+ player_state.last_speed_change_time / 1000
319
+ )
320
+ device_state.position = int(player_state.relative_position / 1000)
321
+ if recording.start_time:
322
+ device_state.start_time = int(
323
+ dt.fromisoformat(
324
+ recording.start_time.replace("Z", "+00:00")
325
+ ).timestamp()
326
+ )
327
+ if recording.end_time:
328
+ device_state.end_time = int(
329
+ dt.fromisoformat(recording.end_time.replace("Z", "+00:00")).timestamp()
330
+ )
331
+ if recording.start_time and recording.end_time:
332
+ device_state.duration = device_state.end_time - device_state.start_time
333
+ if recording.source == LGHorizonRecordingSource.SHOW:
334
+ device_state.show_title = recording.title
335
+ else:
336
+ device_state.show_title = recording.show_title
337
+
338
+ device_state.image = await self._get_intent_image_url(recording.id)
339
+
340
+ async def _get_intent_image_url(self, intent_id: str) -> Optional[str]:
341
+ """Get intent image url."""
342
+ service_config = await self._auth.get_service_config()
343
+ intents_url = await service_config.get_service_url("imageService")
344
+ intents_path = "/intent"
345
+ body_json = [
346
+ {
347
+ "id": intent_id,
348
+ "intents": ["detailedBackground", "posterTile"],
349
+ }
350
+ ]
351
+ intents_body = urllib.parse.quote(
352
+ json.dumps(body_json, separators=(",", ":"), indent=None), safe="~"
353
+ )
354
+
355
+ # Construct the full path with the URL-encoded JSON as a query parameter
356
+ full_intents_path = f"{intents_path}?jsonBody={intents_body}"
357
+ intents_result = await self._auth.request(intents_url, full_intents_path)
358
+ if (
359
+ "intents" in intents_result[0]
360
+ and len(intents_result[0]["intents"]) > 0
361
+ and intents_result[0]["intents"][0]["url"]
362
+ ):
363
+ return intents_result[0]["intents"][0]["url"]
364
+ return None
@@ -0,0 +1,38 @@
1
+ "LG Horizon Message Factory."
2
+
3
+ from .lghorizon_models import (
4
+ LGHorizonMessage,
5
+ LGHorizonStatusMessage,
6
+ LGHorizonUnknownMessage,
7
+ LGHorizonUIStatusMessage,
8
+ LGHorizonMessageType, # Import LGHorizonMessageType from here
9
+ )
10
+
11
+
12
+ class LGHorizonMessageFactory:
13
+ """Handle incoming MQTT messages for LG Horizon devices."""
14
+
15
+ def __init__(self):
16
+ """Initialize the LG Horizon Message Factory."""
17
+
18
+ async def create_message(self, topic: str, payload: dict) -> LGHorizonMessage:
19
+ """Create an LG Horizon message based on the topic and payload."""
20
+ message_type = await self._get_message_type(topic, payload)
21
+ match message_type:
22
+ case LGHorizonMessageType.STATUS:
23
+ return LGHorizonStatusMessage(payload, topic)
24
+ case LGHorizonMessageType.UI_STATUS:
25
+ return LGHorizonUIStatusMessage(payload, topic)
26
+ case LGHorizonMessageType.UNKNOWN:
27
+ return LGHorizonUnknownMessage(payload, topic)
28
+
29
+ async def _get_message_type(
30
+ self, topic: str, payload: dict
31
+ ) -> LGHorizonMessageType:
32
+ """Determine the message type based on topic and payload."""
33
+ if "status" in topic:
34
+ return LGHorizonMessageType.STATUS
35
+ if "type" in payload:
36
+ if payload["type"] == "CPE.uiStatus":
37
+ return LGHorizonMessageType.UI_STATUS
38
+ return LGHorizonMessageType.UNKNOWN