lghorizon 0.9.0.dev3__py3-none-any.whl → 0.9.0.dev4__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/models.py ADDED
@@ -0,0 +1,768 @@
1
+ """Models for LGHorizon API."""
2
+
3
+ # pylint: disable=broad-exception-caught
4
+ # pylint: disable=broad-exception-raised
5
+ from datetime import datetime
6
+ from typing import Callable, Dict
7
+ import json
8
+ import logging
9
+ import paho.mqtt.client as mqtt
10
+
11
+ from .const import (
12
+ BOX_PLAY_STATE_CHANNEL,
13
+ ONLINE_STANDBY,
14
+ ONLINE_RUNNING,
15
+ MEDIA_KEY_POWER,
16
+ MEDIA_KEY_PLAY_PAUSE,
17
+ MEDIA_KEY_STOP,
18
+ MEDIA_KEY_CHANNEL_UP,
19
+ MEDIA_KEY_CHANNEL_DOWN,
20
+ MEDIA_KEY_ENTER,
21
+ MEDIA_KEY_REWIND,
22
+ MEDIA_KEY_FAST_FORWARD,
23
+ MEDIA_KEY_RECORD,
24
+ RECORDING_TYPE_SEASON,
25
+ )
26
+
27
+ from .helpers import make_id
28
+
29
+ _logger = logging.getLogger(__name__)
30
+
31
+
32
+ class LGHorizonAuth:
33
+ """Class to hold LGHorizon authentication."""
34
+
35
+ household_id: str
36
+ access_token: str
37
+ refresh_token: str
38
+ refresh_token_expiry: datetime
39
+ username: str
40
+ mqtt_token: str = None
41
+ access_token: str = None
42
+
43
+ def __init__(self):
44
+ """Initialize a session."""
45
+
46
+ def fill(self, auth_json) -> None:
47
+ """Fill the object."""
48
+ self.household_id = auth_json["householdId"]
49
+ self.access_token = auth_json["accessToken"]
50
+ self.refresh_token = auth_json["refreshToken"]
51
+ self.username = auth_json["username"]
52
+ try:
53
+ self.refresh_token_expiry = datetime.fromtimestamp(
54
+ auth_json["refreshTokenExpiry"]
55
+ )
56
+ except ValueError:
57
+ # VM uses milliseconds for the expiry time.
58
+ # If the year is too high to be valid, it assumes it's milliseconds and divides it
59
+ self.refresh_token_expiry = datetime.fromtimestamp(
60
+ auth_json["refreshTokenExpiry"] // 1000
61
+ )
62
+
63
+ def is_expired(self) -> bool:
64
+ """Check if refresh token is expired."""
65
+ return self.refresh_token_expiry
66
+
67
+
68
+ class LGHorizonPlayingInfo:
69
+ """Represent current state of a box."""
70
+
71
+ channel_id: str = None
72
+ title: str = None
73
+ image: str = None
74
+ source_type: str = None
75
+ paused: bool = False
76
+ channel_title: str = None
77
+ duration: float = None
78
+ position: float = None
79
+ last_position_update: datetime = None
80
+
81
+ def __init__(self):
82
+ """Initialize the playing info."""
83
+
84
+ def set_paused(self, paused: bool):
85
+ """Set pause state."""
86
+ self.paused = paused
87
+
88
+ def set_channel(self, channel_id):
89
+ """Set channel."""
90
+ self.channel_id = channel_id
91
+
92
+ def set_title(self, title):
93
+ """Set title."""
94
+ self.title = title
95
+
96
+ def set_channel_title(self, title):
97
+ """Set channel title."""
98
+ self.channel_title = title
99
+
100
+ def set_image(self, image):
101
+ """Set image."""
102
+ self.image = image
103
+
104
+ def set_source_type(self, source_type):
105
+ """Set source type."""
106
+ self.source_type = source_type
107
+
108
+ def set_duration(self, duration: float):
109
+ """Set duration."""
110
+ self.duration = duration
111
+
112
+ def set_position(self, position: float):
113
+ """Set position."""
114
+ self.position = position
115
+
116
+ def set_last_position_update(self, last_position_update: datetime):
117
+ """Set last position update."""
118
+ self.last_position_update = last_position_update
119
+
120
+ def reset_progress(self):
121
+ """Reset the progress."""
122
+ self.last_position_update = None
123
+ self.duration = None
124
+ self.position = None
125
+
126
+ def reset(self):
127
+ """Reset the channel"""
128
+ self.channel_id = None
129
+ self.title = None
130
+ self.image = None
131
+ self.source_type = None
132
+ self.paused = False
133
+ self.channel_title = None
134
+ self.reset_progress()
135
+
136
+
137
+ class LGHorizonChannel:
138
+ """Represent a channel."""
139
+
140
+ id: str
141
+ title: str
142
+ stream_image: str
143
+ logo_image: str
144
+ channel_number: str
145
+
146
+ def __init__(self, channel_json):
147
+ """Initialize a channel."""
148
+ self.id = channel_json["id"]
149
+ self.title = channel_json["name"]
150
+ self.stream_image = self.get_stream_image(channel_json)
151
+ if "logo" in channel_json and "focused" in channel_json["logo"]:
152
+ self.logo_image = channel_json["logo"]["focused"]
153
+ else:
154
+ self.logo_image = ""
155
+ self.channel_number = channel_json["logicalChannelNumber"]
156
+
157
+ def get_stream_image(self, channel_json) -> str:
158
+ """Returns the stream image."""
159
+ image_stream = channel_json["imageStream"]
160
+ if "full" in image_stream:
161
+ return image_stream["full"]
162
+ if "small" in image_stream:
163
+ return image_stream["small"]
164
+ if "logo" in channel_json and "focused" in channel_json["logo"]:
165
+ return channel_json["logo"]["focused"]
166
+ return ""
167
+
168
+
169
+ class LGHorizonReplayEvent:
170
+ """LGhorizon replay event."""
171
+
172
+ episode_number: int = None
173
+ channel_id: str = None
174
+ event_id: str = None
175
+ season_number: int = None
176
+ title: str = None
177
+ episode_name: str = None
178
+
179
+ def __init__(self, raw_json: str):
180
+ self.channel_id = raw_json["channelId"]
181
+ self.event_id = raw_json["eventId"]
182
+ self.title = raw_json["title"]
183
+ if "episodeName" in raw_json:
184
+ self.episode_name = raw_json["episodeName"]
185
+ if "episodeNumber" in raw_json:
186
+ self.episode_number = raw_json["episodeNumber"]
187
+ if "seasonNumber" in raw_json:
188
+ self.season_number = raw_json["seasonNumber"]
189
+
190
+
191
+ class LGHorizonBaseRecording:
192
+ """LgHorizon base recording."""
193
+
194
+ recording_id: str = None
195
+ title: str = None
196
+ image: str = None
197
+ recording_type: str = None
198
+ channel_id: str = None
199
+
200
+ def __init__(
201
+ self,
202
+ recording_id: str,
203
+ title: str,
204
+ image: str,
205
+ channel_id: str,
206
+ recording_type: str,
207
+ ) -> None:
208
+ self.recording_id = recording_id
209
+ self.title = title
210
+ self.image = image
211
+ self.channel_id = channel_id
212
+ self.recording_type = recording_type
213
+
214
+
215
+ class LGHorizonRecordingSingle(LGHorizonBaseRecording):
216
+ """Represents a single recording."""
217
+
218
+ season_number: int = None
219
+ episode_number: int = None
220
+
221
+ def __init__(self, recording_json):
222
+ """Init the single recording."""
223
+ poster_url = None
224
+ if "poster" in recording_json and "url" in recording_json["poster"]:
225
+ poster_url = recording_json["poster"]["url"]
226
+ LGHorizonBaseRecording.__init__(
227
+ self,
228
+ recording_json["id"],
229
+ recording_json["title"],
230
+ poster_url,
231
+ recording_json["channelId"],
232
+ recording_json["type"],
233
+ )
234
+ if "seasonNumber" in recording_json:
235
+ self.season_number = recording_json["seasonNumber"]
236
+ if "episodeNumber" in recording_json:
237
+ self.episode_number = recording_json["episodeNumber"]
238
+
239
+
240
+ class LGHorizonRecordingEpisode:
241
+ """Represents a single recording."""
242
+
243
+ episode_id: str = None
244
+ episode_title: str = None
245
+ season_number: int = None
246
+ episode_number: int = None
247
+ show_title: str = None
248
+ recording_state: str = None
249
+ image: str = None
250
+
251
+ def __init__(self, recording_json):
252
+ """Init the single recording."""
253
+ self.episode_id = recording_json["episodeId"]
254
+ self.episode_title = recording_json["episodeTitle"]
255
+ self.show_title = recording_json["showTitle"]
256
+ self.recording_state = recording_json["recordingState"]
257
+ if "seasonNumber" in recording_json:
258
+ self.season_number = recording_json["seasonNumber"]
259
+ if "episodeNumber" in recording_json:
260
+ self.episode_number = recording_json["episodeNumber"]
261
+ if "poster" in recording_json and "url" in recording_json["poster"]:
262
+ self.image = recording_json["poster"]["url"]
263
+
264
+
265
+ class LGHorizonRecordingShow:
266
+ """Represents a single recording."""
267
+
268
+ episode_id: str = None
269
+ show_title: str = None
270
+ season_number: int = None
271
+ episode_number: int = None
272
+ recording_state: str = None
273
+ image: str = None
274
+
275
+ def __init__(self, recording_json):
276
+ """Init the single recording."""
277
+ self.episode_id = recording_json["episodeId"]
278
+ self.show_title = recording_json["showTitle"]
279
+ self.recording_state = recording_json["recordingState"]
280
+ if "seasonNumber" in recording_json:
281
+ self.season_number = recording_json["seasonNumber"]
282
+ if "episodeNumber" in recording_json:
283
+ self.episode_number = recording_json["episodeNumber"]
284
+ if "poster" in recording_json and "url" in recording_json["poster"]:
285
+ self.image = recording_json["poster"]["url"]
286
+
287
+
288
+ class LGHorizonRecordingListSeasonShow(LGHorizonBaseRecording):
289
+ """LGHorizon Season show list."""
290
+
291
+ show_id: str = None
292
+
293
+ def __init__(self, recording_season_json):
294
+ """Init the single recording."""
295
+
296
+ poster_url = None
297
+ if (
298
+ "poster" in recording_season_json
299
+ and "url" in recording_season_json["poster"]
300
+ ):
301
+ poster_url = recording_season_json["poster"]["url"]
302
+ LGHorizonBaseRecording.__init__(
303
+ self,
304
+ recording_season_json["id"],
305
+ recording_season_json["title"],
306
+ poster_url,
307
+ recording_season_json["channelId"],
308
+ recording_season_json["type"],
309
+ )
310
+ if self.recording_type == RECORDING_TYPE_SEASON:
311
+ self.show_id = recording_season_json["showId"]
312
+ else:
313
+ self.show_id = recording_season_json["id"]
314
+
315
+
316
+ class LGHorizonVod:
317
+ """LGHorizon video on demand."""
318
+
319
+ title: str = None
320
+ image: str = None
321
+ duration: float = None
322
+
323
+ def __init__(self, vod_json) -> None:
324
+ self.title = vod_json["title"]
325
+ self.duration = vod_json["duration"]
326
+
327
+
328
+ class LGHorizonApp:
329
+ """LGHorizon App."""
330
+
331
+ title: str = None
332
+ image: str = None
333
+
334
+ def __init__(self, app_state_json: str) -> None:
335
+ self.title = app_state_json["appName"]
336
+ self.image = app_state_json["logoPath"]
337
+ if not self.image.startswith("http:"):
338
+ self.image = "https:" + self.image
339
+
340
+
341
+ class LGHorizonMqttClient:
342
+ """LGHorizon MQTT client."""
343
+
344
+ _broker_url: str = None
345
+ _mqtt_client: mqtt.Client
346
+ _auth: LGHorizonAuth
347
+ client_id: str = None
348
+ _on_connected_callback: Callable = None
349
+ _on_message_callback: Callable[[str, str], None] = None
350
+
351
+ @property
352
+ def is_connected(self):
353
+ """Is client connected."""
354
+ return self._mqtt_client.is_connected
355
+
356
+ def __init__(
357
+ self,
358
+ auth: LGHorizonAuth,
359
+ mqtt_broker_url: str,
360
+ on_connected_callback: Callable = None,
361
+ on_message_callback: Callable[[str], None] = None,
362
+ ):
363
+ self._auth = auth
364
+ self._broker_url = mqtt_broker_url.replace("wss://", "").replace(
365
+ ":443/mqtt", ""
366
+ )
367
+ self.client_id = make_id()
368
+ self._mqtt_client = mqtt.Client(
369
+ client_id=self.client_id,
370
+ transport="websockets",
371
+ )
372
+
373
+ self._mqtt_client.ws_set_options(
374
+ headers={"Sec-WebSocket-Protocol": "mqtt, mqttv3.1, mqttv3.11"}
375
+ )
376
+ self._mqtt_client.username_pw_set(
377
+ self._auth.household_id, self._auth.mqtt_token
378
+ )
379
+ self._mqtt_client.tls_set()
380
+ self._mqtt_client.enable_logger(_logger)
381
+ self._mqtt_client.on_connect = self._on_mqtt_connect
382
+ self._on_connected_callback = on_connected_callback
383
+ self._on_message_callback = on_message_callback
384
+
385
+ def _on_mqtt_connect(self, client, userdata, flags, result_code): # pylint: disable=unused-argument
386
+ if result_code == 0:
387
+ self._mqtt_client.on_message = self._on_client_message
388
+ self._mqtt_client.subscribe(self._auth.household_id)
389
+ self._mqtt_client.subscribe(self._auth.household_id + "/#")
390
+ self._mqtt_client.subscribe(self._auth.household_id + "/" + self.client_id)
391
+ self._mqtt_client.subscribe(self._auth.household_id + "/+/status")
392
+ self._mqtt_client.subscribe(
393
+ self._auth.household_id + "/+/networkRecordings"
394
+ )
395
+ self._mqtt_client.subscribe(
396
+ self._auth.household_id + "/+/networkRecordings/capacity"
397
+ )
398
+ self._mqtt_client.subscribe(self._auth.household_id + "/+/localRecordings")
399
+ self._mqtt_client.subscribe(
400
+ self._auth.household_id + "/+/localRecordings/capacity"
401
+ )
402
+ self._mqtt_client.subscribe(self._auth.household_id + "/watchlistService")
403
+ self._mqtt_client.subscribe(self._auth.household_id + "/purchaseService")
404
+ self._mqtt_client.subscribe(
405
+ self._auth.household_id + "/personalizationService"
406
+ )
407
+ self._mqtt_client.subscribe(self._auth.household_id + "/recordingStatus")
408
+ self._mqtt_client.subscribe(
409
+ self._auth.household_id + "/recordingStatus/lastUserAction"
410
+ )
411
+ if self._on_connected_callback:
412
+ self._on_connected_callback()
413
+ elif result_code == 5:
414
+ self._mqtt_client.username_pw_set(
415
+ self._auth.household_id, self._auth.mqtt_token
416
+ )
417
+ self.connect()
418
+ else:
419
+ _logger.error(
420
+ "Cannot connect to MQTT server with resultCode: %s", result_code
421
+ )
422
+
423
+ def connect(self) -> None:
424
+ """Connect the client."""
425
+ self._mqtt_client.connect(self._broker_url, 443)
426
+ self._mqtt_client.loop_start()
427
+
428
+ def _on_client_message(self, client, userdata, message): # pylint: disable=unused-argument
429
+ """Handle messages received by mqtt client."""
430
+ _logger.debug("Received MQTT message. Topic: %s", message.topic)
431
+ json_payload = json.loads(message.payload)
432
+ _logger.debug("Message: %s", json_payload)
433
+ if self._on_message_callback:
434
+ self._on_message_callback(json_payload, message.topic)
435
+
436
+ def publish_message(self, topic: str, json_payload: str) -> None:
437
+ """Publish a MQTT message."""
438
+ self._mqtt_client.publish(topic, json_payload, qos=2)
439
+
440
+ def disconnect(self) -> None:
441
+ """Disconnect the client."""
442
+ if self._mqtt_client.is_connected():
443
+ self._mqtt_client.disconnect()
444
+
445
+
446
+ class LGHorizonBox:
447
+ """The LGHorizon box."""
448
+
449
+ device_id: str = None
450
+ hashed_cpe_id: str = None
451
+ device_friendly_name: str = None
452
+ state: str = None
453
+ playing_info: LGHorizonPlayingInfo = None
454
+ manufacturer: str = None
455
+ model: str = None
456
+ recording_capacity: int = None
457
+
458
+ _mqtt_client: LGHorizonMqttClient
459
+ _change_callback: Callable = None
460
+ _auth: LGHorizonAuth = None
461
+ _channels: Dict[str, LGHorizonChannel] = None
462
+ _message_stamp = None
463
+
464
+ def __init__(
465
+ self,
466
+ box_json: str,
467
+ platform_type: Dict[str, str],
468
+ mqtt_client: LGHorizonMqttClient,
469
+ auth: LGHorizonAuth,
470
+ channels: Dict[str, LGHorizonChannel],
471
+ ):
472
+ self.device_id = box_json["deviceId"]
473
+ self.hashed_cpe_id = box_json["hashedCPEId"]
474
+ self.device_friendly_name = box_json["settings"]["deviceFriendlyName"]
475
+ self._mqtt_client = mqtt_client
476
+ self._auth = auth
477
+ self._channels = channels
478
+ self.playing_info = LGHorizonPlayingInfo()
479
+ if platform_type:
480
+ self.manufacturer = platform_type["manufacturer"]
481
+ self.model = platform_type["model"]
482
+
483
+ def update_channels(self, channels: Dict[str, LGHorizonChannel]):
484
+ """Update the channels list."""
485
+ self._channels = channels
486
+
487
+ def register_mqtt(self) -> None:
488
+ """Register the mqtt connection."""
489
+ if not self._mqtt_client.is_connected:
490
+ raise Exception("MQTT client not connected.")
491
+ topic = f"{self._auth.household_id}/{self._mqtt_client.client_id}/status"
492
+ payload = {
493
+ "source": self._mqtt_client.client_id,
494
+ "state": ONLINE_RUNNING,
495
+ "deviceType": "HGO",
496
+ }
497
+ self._mqtt_client.publish_message(topic, json.dumps(payload))
498
+
499
+ def set_callback(self, change_callback: Callable) -> None:
500
+ """Set a callback function."""
501
+ self._change_callback = change_callback
502
+
503
+ def update_state(self, payload):
504
+ """Register a new settop box."""
505
+ state = payload["state"]
506
+ if self.state == state:
507
+ return
508
+ self.state = state
509
+ if state == ONLINE_STANDBY:
510
+ self.playing_info.reset()
511
+ if self._change_callback:
512
+ self._change_callback(self.device_id)
513
+ else:
514
+ self._request_settop_box_state()
515
+ self._request_settop_box_recording_capacity()
516
+
517
+ def update_recording_capacity(self, payload) -> None:
518
+ """Updates the recording capacity."""
519
+ if "CPE.capacity" not in payload or "used" not in payload:
520
+ return
521
+ self.recording_capacity = payload["used"]
522
+
523
+ def update_with_replay_event(
524
+ self, source_type: str, event: LGHorizonReplayEvent, channel: LGHorizonChannel
525
+ ) -> None:
526
+ """Update box with replay event."""
527
+ self.playing_info.set_source_type(source_type)
528
+ self.playing_info.set_channel(channel.id)
529
+ self.playing_info.set_channel_title(channel.title)
530
+ title = event.title
531
+ if event.episode_name:
532
+ title += f": {event.episode_name}"
533
+ self.playing_info.set_title(title)
534
+ self.playing_info.set_image(channel.stream_image)
535
+ self.playing_info.reset_progress()
536
+ self._trigger_callback()
537
+
538
+ def update_with_recording(
539
+ self,
540
+ source_type: str,
541
+ recording: LGHorizonRecordingSingle,
542
+ channel: LGHorizonChannel,
543
+ start: float,
544
+ end: float,
545
+ last_speed_change: float,
546
+ relative_position: float,
547
+ ) -> None:
548
+ """Update box with recording."""
549
+ self.playing_info.set_source_type(source_type)
550
+ self.playing_info.set_channel(channel.id)
551
+ self.playing_info.set_channel_title(channel.title)
552
+ self.playing_info.set_title(f"{recording.title}")
553
+ self.playing_info.set_image(recording.image)
554
+ start_dt = datetime.fromtimestamp(start / 1000.0)
555
+ end_dt = datetime.fromtimestamp(end / 1000.0)
556
+ duration = (end_dt - start_dt).total_seconds()
557
+ self.playing_info.set_duration(duration)
558
+ self.playing_info.set_position(relative_position / 1000.0)
559
+ last_update_dt = datetime.fromtimestamp(last_speed_change / 1000.0)
560
+ self.playing_info.set_last_position_update(last_update_dt)
561
+ self._trigger_callback()
562
+
563
+ def update_with_vod(
564
+ self,
565
+ source_type: str,
566
+ vod: LGHorizonVod,
567
+ last_speed_change: float,
568
+ relative_position: float,
569
+ ) -> None:
570
+ """Update box with vod."""
571
+ self.playing_info.set_source_type(source_type)
572
+ self.playing_info.set_channel(None)
573
+ self.playing_info.set_channel_title(None)
574
+ self.playing_info.set_title(vod.title)
575
+ self.playing_info.set_image(None)
576
+ self.playing_info.set_duration(vod.duration)
577
+ self.playing_info.set_position(relative_position / 1000.0)
578
+ last_update_dt = datetime.fromtimestamp(last_speed_change / 1000.0)
579
+ self.playing_info.set_last_position_update(last_update_dt)
580
+ self._trigger_callback()
581
+
582
+ def update_with_app(self, source_type: str, app: LGHorizonApp) -> None:
583
+ """Update box with app."""
584
+ self.playing_info.set_source_type(source_type)
585
+ self.playing_info.set_channel(None)
586
+ self.playing_info.set_channel_title(app.title)
587
+ self.playing_info.set_title(app.title)
588
+ self.playing_info.set_image(app.image)
589
+ self.playing_info.reset_progress()
590
+ self._trigger_callback()
591
+
592
+ def _trigger_callback(self):
593
+ if self._change_callback:
594
+ _logger.debug("Callback called from box %s", self.device_id)
595
+ self._change_callback(self.device_id)
596
+
597
+ def turn_on(self) -> None:
598
+ """Turn the settop box on."""
599
+
600
+ if self.state == ONLINE_STANDBY:
601
+ self.send_key_to_box(MEDIA_KEY_POWER)
602
+
603
+ def turn_off(self) -> None:
604
+ """Turn the settop box off."""
605
+ if self.state == ONLINE_RUNNING:
606
+ self.send_key_to_box(MEDIA_KEY_POWER)
607
+ self.playing_info.reset()
608
+
609
+ def pause(self) -> None:
610
+ """Pause the given settopbox."""
611
+ if self.state == ONLINE_RUNNING and not self.playing_info.paused:
612
+ self.send_key_to_box(MEDIA_KEY_PLAY_PAUSE)
613
+
614
+ def play(self) -> None:
615
+ """Resume the settopbox."""
616
+ if self.state == ONLINE_RUNNING and self.playing_info.paused:
617
+ self.send_key_to_box(MEDIA_KEY_PLAY_PAUSE)
618
+
619
+ def stop(self) -> None:
620
+ """Stop the settopbox."""
621
+ if self.state == ONLINE_RUNNING:
622
+ self.send_key_to_box(MEDIA_KEY_STOP)
623
+
624
+ def next_channel(self):
625
+ """Select the next channel for given settop box."""
626
+ if self.state == ONLINE_RUNNING:
627
+ self.send_key_to_box(MEDIA_KEY_CHANNEL_UP)
628
+
629
+ def previous_channel(self) -> None:
630
+ """Select the previous channel for given settop box."""
631
+ if self.state == ONLINE_RUNNING:
632
+ self.send_key_to_box(MEDIA_KEY_CHANNEL_DOWN)
633
+
634
+ def press_enter(self) -> None:
635
+ """Press enter on the settop box."""
636
+ if self.state == ONLINE_RUNNING:
637
+ self.send_key_to_box(MEDIA_KEY_ENTER)
638
+
639
+ def rewind(self) -> None:
640
+ """Rewind the settop box."""
641
+ if self.state == ONLINE_RUNNING:
642
+ self.send_key_to_box(MEDIA_KEY_REWIND)
643
+
644
+ def fast_forward(self) -> None:
645
+ """Fast forward the settop box."""
646
+ if self.state == ONLINE_RUNNING:
647
+ self.send_key_to_box(MEDIA_KEY_FAST_FORWARD)
648
+
649
+ def record(self):
650
+ """Record on the settop box."""
651
+ if self.state == ONLINE_RUNNING:
652
+ self.send_key_to_box(MEDIA_KEY_RECORD)
653
+
654
+ def is_available(self) -> bool:
655
+ """Return the availability of the settop box."""
656
+ return self.state == ONLINE_RUNNING or self.state == ONLINE_STANDBY
657
+
658
+ def set_channel(self, source: str) -> None:
659
+ """Change te channel from the settopbox."""
660
+ channel = [src for src in self._channels.values() if src.title == source][0]
661
+ payload = (
662
+ '{"id":"'
663
+ + make_id(8)
664
+ + '","type":"CPE.pushToTV","source":{"clientId":"'
665
+ + self._mqtt_client.client_id
666
+ + '","friendlyDeviceName":"Home Assistant"},'
667
+ + '"status":{"sourceType":"linear","source":{"channelId":"'
668
+ + channel.id
669
+ + '"},"relativePosition":0,"speed":1}}'
670
+ )
671
+
672
+ self._mqtt_client.publish_message(
673
+ f"{self._auth.household_id}/{self.device_id}", payload
674
+ )
675
+
676
+ def play_recording(self, recording_id):
677
+ """Play recording."""
678
+ payload = (
679
+ '{"id":"'
680
+ + make_id(8)
681
+ + '","type":"CPE.pushToTV","source":{"clientId":"'
682
+ + self._mqtt_client.client_id
683
+ + '","friendlyDeviceName":"Home Assistant"},'
684
+ + '"status":{"sourceType":"nDVR","source":{"recordingId":"'
685
+ + recording_id
686
+ + '"},"relativePosition":0}}'
687
+ )
688
+ self._mqtt_client.publish_message(
689
+ f"{self._auth.household_id}/{self.device_id}", payload
690
+ )
691
+
692
+ def send_key_to_box(self, key: str) -> None:
693
+ """Send emulated (remote) key press to settopbox."""
694
+ payload_dict = {
695
+ "type": "CPE.KeyEvent",
696
+ "runtimeType": "key",
697
+ "id": "ha",
698
+ "source": self.device_id.lower(),
699
+ "status": {"w3cKey": key, "eventType": "keyDownUp"},
700
+ }
701
+ payload = json.dumps(payload_dict)
702
+ self._mqtt_client.publish_message(
703
+ f"{self._auth.household_id}/{self.device_id}", payload
704
+ )
705
+
706
+ def _set_unknown_channel_info(self) -> None:
707
+ """Set unknown channel info."""
708
+ _logger.warning("Couldn't set channel. Channel info set to unknown...")
709
+ self.playing_info.set_source_type(BOX_PLAY_STATE_CHANNEL)
710
+ self.playing_info.set_channel(None)
711
+ self.playing_info.set_title("No information available")
712
+ self.playing_info.set_image(None)
713
+ self.playing_info.set_paused(False)
714
+
715
+ def _request_settop_box_state(self) -> None:
716
+ """Send mqtt message to receive state from settop box."""
717
+ topic = f"{self._auth.household_id}/{self.device_id}"
718
+ payload = {
719
+ "id": make_id(8),
720
+ "type": "CPE.getUiStatus",
721
+ "source": self._mqtt_client.client_id,
722
+ }
723
+ self._mqtt_client.publish_message(topic, json.dumps(payload))
724
+
725
+ def _request_settop_box_recording_capacity(self) -> None:
726
+ """Send mqtt message to receive state from settop box."""
727
+ topic = f"{self._auth.household_id}/{self.device_id}"
728
+ payload = {
729
+ "id": make_id(8),
730
+ "type": "CPE.capacity",
731
+ "source": self._mqtt_client.client_id,
732
+ }
733
+ self._mqtt_client.publish_message(topic, json.dumps(payload))
734
+
735
+
736
+ class LGHorizonProfile:
737
+ """LGHorizon profile."""
738
+
739
+ profile_id: str = None
740
+ name: str = None
741
+ favorite_channels: list[str] = None
742
+
743
+ def __init__(self, json_payload):
744
+ self.profile_id = json_payload["profileId"]
745
+ self.name = json_payload["name"]
746
+ self.favorite_channels = json_payload["favoriteChannels"]
747
+
748
+
749
+ class LGHorizonCustomer:
750
+ """LGHorizon customer"""
751
+
752
+ customer_id: str = None
753
+ hashed_customer_id: str = None
754
+ country_id: str = None
755
+ city_id: int = 0
756
+ settop_boxes: list[str] = None
757
+ profiles: Dict[str, LGHorizonProfile] = {}
758
+
759
+ def __init__(self, json_payload):
760
+ self.customer_id = json_payload["customerId"]
761
+ self.hashed_customer_id = json_payload["hashedCustomerId"]
762
+ self.country_id = json_payload["countryId"]
763
+ self.city_id = json_payload["cityId"]
764
+ if "assignedDevices" in json_payload:
765
+ self.settop_boxes = json_payload["assignedDevices"]
766
+ if "profiles" in json_payload:
767
+ for profile in json_payload["profiles"]:
768
+ self.profiles[profile["profileId"]] = LGHorizonProfile(profile)