lghorizon 0.9.0.dev4__py3-none-any.whl → 0.9.1__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 +58 -25
- lghorizon/const.py +23 -81
- lghorizon/exceptions.py +3 -3
- lghorizon/helpers.py +1 -1
- lghorizon/lghorizon_api.py +263 -502
- lghorizon/lghorizon_device.py +409 -0
- lghorizon/lghorizon_device_state_processor.py +365 -0
- lghorizon/lghorizon_message_factory.py +38 -0
- lghorizon/lghorizon_models.py +1512 -0
- lghorizon/lghorizon_mqtt_client.py +335 -0
- lghorizon/lghorizon_recording_factory.py +55 -0
- lghorizon-0.9.1.dist-info/METADATA +189 -0
- lghorizon-0.9.1.dist-info/RECORD +17 -0
- lghorizon/models.py +0 -768
- lghorizon-0.9.0.dev4.dist-info/METADATA +0 -41
- lghorizon-0.9.0.dev4.dist-info/RECORD +0 -12
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/WHEEL +0 -0
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/top_level.txt +0 -0
lghorizon/models.py
DELETED
|
@@ -1,768 +0,0 @@
|
|
|
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)
|