lghorizon 0.9.0.dev1__py3-none-any.whl → 0.9.0.dev2__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 CHANGED
@@ -1,36 +1,3 @@
1
1
  """Python client for LG Horizon."""
2
2
 
3
- from .lghorizonapi import LGHorizonApi
4
- from .models.lghorizon_auth import (
5
- LGHorizonAuth,
6
- )
7
- from .models.exceptions import (
8
- LGHorizonApiUnauthorizedError,
9
- LGHorizonApiConnectionError,
10
- LGHorizonApiLockedError,
11
- )
12
- from .const import (
13
- ONLINE_RUNNING,
14
- ONLINE_STANDBY,
15
- RECORDING_TYPE_SHOW,
16
- RECORDING_TYPE_SEASON,
17
- RECORDING_TYPE_SINGLE,
18
- )
19
-
20
- __all__ = [
21
- "LGHorizonApi",
22
- "LGHorizonBox",
23
- "LGHorizonRecordingListSeasonShow",
24
- "LGHorizonRecordingSingle",
25
- "LGHorizonRecordingShow",
26
- "LGHorizonRecordingEpisode",
27
- "LGHorizonCustomer",
28
- "LGHorizonApiUnauthorizedError",
29
- "LGHorizonApiConnectionError",
30
- "LGHorizonApiLockedError",
31
- "ONLINE_RUNNING",
32
- "ONLINE_STANDBY",
33
- "RECORDING_TYPE_SHOW",
34
- "RECORDING_TYPE_SEASON",
35
- "RECORDING_TYPE_SINGLE",
36
- ] # noqa
3
+ pass
@@ -0,0 +1,17 @@
1
+ """Exceptions for the LGHorizon API."""
2
+
3
+
4
+ class LGHorizonApiError(Exception):
5
+ """Generic LGHorizon exception."""
6
+
7
+
8
+ class LGHorizonApiConnectionError(LGHorizonApiError):
9
+ """Generic LGHorizon exception."""
10
+
11
+
12
+ class LGHorizonApiUnauthorizedError(Exception):
13
+ """Generic LGHorizon exception."""
14
+
15
+
16
+ class LGHorizonApiLockedError(LGHorizonApiUnauthorizedError):
17
+ """Generic LGHorizon exception."""
@@ -3,21 +3,21 @@
3
3
  import logging
4
4
  from typing import Any, Dict, cast
5
5
 
6
- from .models.lghorizon_device import LGHorizonDevice
7
- from .models.lghorizon_channel import LGHorizonChannel
8
- from .models.lghorizon_auth import LGHorizonAuth
9
- from .models.lghorizon_customer import LGHorizonCustomer
10
- from .models.lghorizon_mqtt_client import LGHorizonMqttClient
11
- from .models.lghorizon_config import LGHorizonServicesConfig
12
- from .models.lghorizon_entitlements import LGHorizonEntitlements
13
- from .models.lghorizon_profile import LGHorizonProfile
14
- from .models.lghorizon_message import LGHorizonMessageType
15
- from .message_factory import LGHorizonMessageFactory
16
- from .models.lghorizon_message import LGHorizonStatusMessage, LGHorizonUIStatusMessage
17
- from .models.lghorizon_device_state import LGHorizonRunningState
18
- from .models.lghorizon_recordings import LGHorizonRecordingList, LGHorizonRecordingQuota
19
- from .recording_factory import LGHorizonRecordingFactory
20
- from .device_state_processor import LGHorizonDeviceStateProcessor
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
21
21
 
22
22
 
23
23
  _LOGGER = logging.getLogger(__name__)
@@ -0,0 +1,336 @@
1
+ """LG Horizon Device."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any, Callable, Coroutine, Dict, Optional
8
+ from .lghorizon_models import (
9
+ LGHorizonRunningState,
10
+ LGHorizonStatusMessage,
11
+ LGHorizonUIStatusMessage,
12
+ LGHorizonDeviceState,
13
+ LGHorizonAuth,
14
+ LGHorizonChannel,
15
+ )
16
+
17
+ from .exceptions import LGHorizonApiConnectionError
18
+ from .helpers import make_id
19
+ from .lghorizon_device_state_processor import LGHorizonDeviceStateProcessor
20
+ from .lghorizon_mqtt_client import LGHorizonMqttClient
21
+ from .const import (
22
+ MEDIA_KEY_CHANNEL_DOWN,
23
+ MEDIA_KEY_CHANNEL_UP,
24
+ MEDIA_KEY_ENTER,
25
+ MEDIA_KEY_FAST_FORWARD,
26
+ MEDIA_KEY_PLAY_PAUSE,
27
+ MEDIA_KEY_POWER,
28
+ MEDIA_KEY_RECORD,
29
+ MEDIA_KEY_REWIND,
30
+ MEDIA_KEY_STOP,
31
+ ONLINE_RUNNING,
32
+ PLATFORM_TYPES,
33
+ )
34
+
35
+ _LOGGER = logging.getLogger(__name__)
36
+
37
+
38
+ class LGHorizonDevice:
39
+ """The LG Horizon device (set-top box)."""
40
+
41
+ _device_id: str
42
+ _hashed_cpe_id: str
43
+ _device_friendly_name: str
44
+ _platform_type: str
45
+ _device_state: LGHorizonDeviceState
46
+ _manufacturer: Optional[str]
47
+ _model: Optional[str]
48
+ _recording_capacity: Optional[int]
49
+ _device_state_processor: LGHorizonDeviceStateProcessor
50
+ _mqtt_client: LGHorizonMqttClient
51
+ _change_callback: Callable[[str], Coroutine[Any, Any, Any]]
52
+ _auth: LGHorizonAuth
53
+ _channels: Dict[str, LGHorizonChannel]
54
+ _last_ui_message_timestamp: int = 0
55
+
56
+ def __init__(
57
+ self,
58
+ device_json,
59
+ mqtt_client: LGHorizonMqttClient,
60
+ device_state_processor: LGHorizonDeviceStateProcessor,
61
+ auth: LGHorizonAuth,
62
+ channels: Dict[str, LGHorizonChannel],
63
+ ):
64
+ """Initialize the LG Horizon device."""
65
+ self._device_id = device_json["deviceId"]
66
+ self._hashed_cpe_id = device_json["hashedCPEId"]
67
+ self._device_friendly_name = device_json["settings"]["deviceFriendlyName"]
68
+ self._platform_type = device_json.get("platformType")
69
+ self._mqtt_client = mqtt_client
70
+ self._auth = auth
71
+ self._channels = channels
72
+ self._device_state = LGHorizonDeviceState() # Initialize state
73
+ self._manufacturer = None
74
+ self._model = None
75
+ self._recording_capacity = None
76
+ self._device_state_processor = device_state_processor
77
+
78
+ @property
79
+ def device_id(self) -> str:
80
+ """Return the device ID."""
81
+ return self._device_id
82
+
83
+ @property
84
+ def platform_type(self) -> str:
85
+ """Return the device ID."""
86
+ return self._platform_type
87
+
88
+ @property
89
+ def manufacturer(self) -> str:
90
+ """Return the manufacturer of the settop box."""
91
+ platform_info = PLATFORM_TYPES.get(self._platform_type, dict())
92
+ return platform_info.get("manufacturer", "unknown")
93
+
94
+ @property
95
+ def model(self) -> str:
96
+ """Return the model of the settop box."""
97
+ platform_info = PLATFORM_TYPES.get(self._platform_type, dict())
98
+ return platform_info.get("model", "unknown")
99
+
100
+ @property
101
+ def is_available(self) -> bool:
102
+ """Return the availability of the settop box."""
103
+ return self._device_state.state in (
104
+ LGHorizonRunningState.ONLINE_RUNNING,
105
+ LGHorizonRunningState.ONLINE_STANDBY,
106
+ )
107
+
108
+ @property
109
+ def hashed_cpe_id(self) -> str:
110
+ """Return the hashed CPE ID."""
111
+ return self._hashed_cpe_id
112
+
113
+ @property
114
+ def device_friendly_name(self) -> str:
115
+ """Return the device friendly name."""
116
+ return self._device_friendly_name
117
+
118
+ @property
119
+ def device_state(self) -> LGHorizonDeviceState:
120
+ """Return the current playing information."""
121
+ return self._device_state
122
+
123
+ @property
124
+ def recording_capacity(self) -> Optional[int]:
125
+ """Return the recording capacity used."""
126
+ return self._recording_capacity
127
+
128
+ @recording_capacity.setter
129
+ def recording_capacity(self, value: int) -> None:
130
+ """Set the recording capacity used."""
131
+ self._recording_capacity = value
132
+
133
+ @property
134
+ def last_ui_message_timestamp(self) -> int:
135
+ """Return the last ui message timestamp."""
136
+ return self._last_ui_message_timestamp
137
+
138
+ @last_ui_message_timestamp.setter
139
+ def last_ui_message_timestamp(self, value: int) -> None:
140
+ """Set the last ui message timestamp."""
141
+ self._last_ui_message_timestamp = value
142
+
143
+ async def update_channels(self, channels: Dict[str, LGHorizonChannel]):
144
+ """Update the channels list."""
145
+ self._channels = channels
146
+
147
+ async def register_mqtt(self) -> None:
148
+ """Register the mqtt connection."""
149
+ if not self._mqtt_client.is_connected:
150
+ raise LGHorizonApiConnectionError("MQTT client not connected.")
151
+ topic = f"{self._auth.household_id}/{self._mqtt_client.client_id}/status"
152
+ payload = {
153
+ "source": self._mqtt_client.client_id,
154
+ "state": ONLINE_RUNNING,
155
+ "deviceType": "HGO",
156
+ }
157
+ await self._mqtt_client.publish_message(topic, json.dumps(payload))
158
+
159
+ async def set_callback(
160
+ self, change_callback: Callable[[str], Coroutine[Any, Any, Any]]
161
+ ) -> None:
162
+ """Set a callback function."""
163
+ self._change_callback = change_callback
164
+ await self.register_mqtt() # type: ignore [assignment] # Callback can be None
165
+
166
+ async def handle_status_message(
167
+ self, status_message: LGHorizonStatusMessage
168
+ ) -> None:
169
+ """Register a new settop box."""
170
+ old_running_state = self.device_state.state
171
+ new_running_state = status_message.running_state
172
+ if (
173
+ old_running_state == new_running_state
174
+ ): # Access backing field for comparison
175
+ return
176
+ await self._device_state_processor.process_state(
177
+ self.device_state, status_message
178
+ ) # Use the setter
179
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
180
+ await self._request_settop_box_state()
181
+
182
+ await self._trigger_callback()
183
+ await self._request_settop_box_recording_capacity()
184
+
185
+ async def handle_ui_status_message(
186
+ self, status_message: LGHorizonUIStatusMessage
187
+ ) -> None:
188
+ """Handle UI status message."""
189
+
190
+ await self._device_state_processor.process_ui_state(
191
+ self.device_state, status_message
192
+ )
193
+ self.last_ui_message_timestamp = status_message.message_timestamp
194
+ await self._trigger_callback()
195
+
196
+ async def update_recording_capacity(self, payload) -> None:
197
+ """Updates the recording capacity."""
198
+ if "CPE.capacity" not in payload or "used" not in payload:
199
+ return
200
+ self.recording_capacity = payload["used"] # Use the setter
201
+
202
+ async def _trigger_callback(self):
203
+ if self._change_callback:
204
+ _LOGGER.debug("Callback called from box %s", self.device_id)
205
+ await self._change_callback(self.device_id)
206
+
207
+ async def turn_on(self) -> None:
208
+ """Turn the settop box on."""
209
+
210
+ if self._device_state.state == LGHorizonRunningState.ONLINE_STANDBY:
211
+ await self.send_key_to_box(MEDIA_KEY_POWER)
212
+
213
+ async def turn_off(self) -> None:
214
+ """Turn the settop box off."""
215
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
216
+ await self.send_key_to_box(MEDIA_KEY_POWER)
217
+ await self._device_state.reset()
218
+
219
+ async def pause(self) -> None:
220
+ """Pause the given settopbox."""
221
+ if (
222
+ self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING
223
+ and not self._device_state.paused
224
+ ):
225
+ await self.send_key_to_box(MEDIA_KEY_PLAY_PAUSE)
226
+
227
+ async def play(self) -> None:
228
+ """Resume the settopbox."""
229
+ if (
230
+ self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING
231
+ and self._device_state.paused
232
+ ):
233
+ await self.send_key_to_box(MEDIA_KEY_PLAY_PAUSE)
234
+
235
+ async def stop(self) -> None:
236
+ """Stop the settopbox."""
237
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
238
+ await self.send_key_to_box(MEDIA_KEY_STOP)
239
+
240
+ async def next_channel(self):
241
+ """Select the next channel for given settop box."""
242
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
243
+ await self.send_key_to_box(MEDIA_KEY_CHANNEL_UP)
244
+
245
+ async def previous_channel(self) -> None:
246
+ """Select the previous channel for given settop box."""
247
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
248
+ await self.send_key_to_box(MEDIA_KEY_CHANNEL_DOWN)
249
+
250
+ async def press_enter(self) -> None:
251
+ """Press enter on the settop box."""
252
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
253
+ await self.send_key_to_box(MEDIA_KEY_ENTER)
254
+
255
+ async def rewind(self) -> None:
256
+ """Rewind the settop box."""
257
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
258
+ await self.send_key_to_box(MEDIA_KEY_REWIND)
259
+
260
+ async def fast_forward(self) -> None:
261
+ """Fast forward the settop box."""
262
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
263
+ await self.send_key_to_box(MEDIA_KEY_FAST_FORWARD)
264
+
265
+ async def record(self):
266
+ """Record on the settop box."""
267
+ if self._device_state.state == LGHorizonRunningState.ONLINE_RUNNING:
268
+ await self.send_key_to_box(MEDIA_KEY_RECORD)
269
+
270
+ async def set_channel(self, source: str) -> None:
271
+ """Change te channel from the settopbox."""
272
+ channel = [src for src in self._channels.values() if src.title == source][0]
273
+ payload = (
274
+ '{"id":"'
275
+ + await make_id(8)
276
+ + '","type":"CPE.pushToTV","source":{"clientId":"'
277
+ + self._mqtt_client.client_id
278
+ + '","friendlyDeviceName":"Home Assistant"},'
279
+ + '"status":{"sourceType":"linear","source":{"channelId":"'
280
+ + channel.id
281
+ + '"},"relativePosition":0,"speed":1}}'
282
+ )
283
+
284
+ await self._mqtt_client.publish_message(
285
+ f"{self._auth.household_id}/{self.device_id}", payload
286
+ )
287
+
288
+ async def play_recording(self, recording_id):
289
+ """Play recording."""
290
+ payload = (
291
+ '{"id":"'
292
+ + await make_id(8)
293
+ + '","type":"CPE.pushToTV","source":{"clientId":"'
294
+ + self._mqtt_client.client_id
295
+ + '","friendlyDeviceName":"Home Assistant"},'
296
+ + '"status":{"sourceType":"nDVR","source":{"recordingId":"'
297
+ + recording_id
298
+ + '"},"relativePosition":0}}'
299
+ )
300
+ await self._mqtt_client.publish_message(
301
+ f"{self._auth.household_id}/{self.device_id}", payload
302
+ )
303
+
304
+ async def send_key_to_box(self, key: str) -> None:
305
+ """Send emulated (remote) key press to settopbox."""
306
+ payload_dict = {
307
+ "type": "CPE.KeyEvent",
308
+ "runtimeType": "key",
309
+ "id": "ha",
310
+ "source": self.device_id.lower(),
311
+ "status": {"w3cKey": key, "eventType": "keyDownUp"},
312
+ }
313
+ payload = json.dumps(payload_dict)
314
+ await self._mqtt_client.publish_message(
315
+ f"{self._auth.household_id}/{self.device_id}", payload
316
+ )
317
+
318
+ async def _request_settop_box_state(self) -> None:
319
+ """Send mqtt message to receive state from settop box."""
320
+ topic = f"{self._auth.household_id}/{self.device_id}"
321
+ payload = {
322
+ "id": await make_id(8),
323
+ "type": "CPE.getUiStatus",
324
+ "source": self._mqtt_client.client_id,
325
+ }
326
+ await self._mqtt_client.publish_message(topic, json.dumps(payload))
327
+
328
+ async def _request_settop_box_recording_capacity(self) -> None:
329
+ """Send mqtt message to receive state from settop box."""
330
+ topic = f"{self._auth.household_id}/{self.device_id}"
331
+ payload = {
332
+ "id": await make_id(8),
333
+ "type": "CPE.capacity",
334
+ "source": self._mqtt_client.client_id,
335
+ }
336
+ await self._mqtt_client.publish_message(topic, json.dumps(payload))
@@ -6,9 +6,9 @@ import urllib.parse
6
6
 
7
7
  from typing import cast, Dict, Optional
8
8
 
9
- from .models.lghorizon_device_state import LGHorizonDeviceState, LGHorizonRunningState
10
- from .models.lghorizon_message import LGHorizonStatusMessage, LGHorizonUIStatusMessage
11
- from .models.lghorizon_sources import (
9
+ from .lghorizon_models import LGHorizonDeviceState, LGHorizonRunningState
10
+ from .lghorizon_models import LGHorizonStatusMessage, LGHorizonUIStatusMessage
11
+ from .lghorizon_models import (
12
12
  LGHorizonSourceType,
13
13
  LGHorizonLinearSource,
14
14
  LGHorizonVODSource,
@@ -16,20 +16,20 @@ from .models.lghorizon_sources import (
16
16
  LGHorizonNDVRSource,
17
17
  LGHorizonReviewBufferSource,
18
18
  )
19
- from .models.lghorizon_auth import LGHorizonAuth
20
- from .models.lghorizon_events import (
19
+ from .lghorizon_models import LGHorizonAuth
20
+ from .lghorizon_models import (
21
21
  LGHorizonReplayEvent,
22
22
  LGHorizonVOD,
23
23
  )
24
24
 
25
- from .models.lghorizon_recordings import LGHorizonRecordingSingle
26
- from .models.lghorizon_channel import LGHorizonChannel
27
- from .models.lghorizon_ui_status import (
25
+ from .lghorizon_models import LGHorizonRecordingSingle
26
+ from .lghorizon_models import LGHorizonChannel
27
+ from .lghorizon_models import (
28
28
  LGHorizonUIStateType,
29
29
  LGHorizonAppsState,
30
30
  LGHorizonPlayerState,
31
31
  )
32
- from .models.lghorizon_customer import LGHorizonCustomer
32
+ from .lghorizon_models import LGHorizonCustomer
33
33
 
34
34
 
35
35
  class LGHorizonDeviceStateProcessor:
@@ -1,6 +1,6 @@
1
1
  "LG Horizon Message Factory."
2
2
 
3
- from .models.lghorizon_message import (
3
+ from .lghorizon_models import (
4
4
  LGHorizonMessage,
5
5
  LGHorizonStatusMessage,
6
6
  LGHorizonUnknownMessage,