File: directv.py

package info (click to toggle)
python-directv 0.4.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 324 kB
  • sloc: python: 1,067; sh: 5; makefile: 3
file content (236 lines) | stat: -rw-r--r-- 7,695 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""Asynchronous Python client for DirecTV."""
import asyncio
import json
from socket import gaierror as SocketGIAEroor
from typing import Any, Mapping, Optional

import aiohttp
from yarl import URL

from .__version__ import __version__
from .const import VALID_REMOTE_KEYS
from .exceptions import DIRECTVAccessRestricted, DIRECTVConnectionError, DIRECTVError
from .models import Device, Program, State
from .utils import parse_channel_number


class DIRECTV:
    """Main class for handling connections with DirecTV servers."""

    _device: Optional[Device] = None

    def __init__(
        self,
        host: str,
        base_path: str = "/",
        password: str = None,
        port: int = 8080,
        request_timeout: int = 8,
        session: aiohttp.client.ClientSession = None,
        username: str = None,
        user_agent: str = None,
    ) -> None:
        """Initialize connection with receiver."""
        self._session = session
        self._close_session = False

        self.base_path = base_path
        self.host = host
        self.password = password
        self.port = port
        self.request_timeout = request_timeout
        self.username = username
        self.user_agent = user_agent

        if user_agent is None:
            self.user_agent = f"PythonDirecTV/{__version__}"

    async def _request(
        self,
        uri: str = "",
        method: str = "GET",
        data: Optional[Any] = None,
        params: Optional[Mapping[str, str]] = None,
    ) -> Any:
        """Handle a request to a receiver."""
        scheme = "http"

        url = URL.build(
            scheme=scheme, host=self.host, port=self.port, path=self.base_path
        ).join(URL(uri))

        auth = None
        if self.username and self.password:
            auth = aiohttp.BasicAuth(self.username, self.password)

        headers = {
            "User-Agent": self.user_agent,
            "Accept": "application/json, text/plain, */*",
        }

        if self._session is None:
            self._session = aiohttp.ClientSession()
            self._close_session = True

        try:
            async with asyncio.timeout(self.request_timeout):
                response = await self._session.request(
                    method, url, auth=auth, data=data, params=params, headers=headers,
                )
        except asyncio.TimeoutError as exception:
            raise DIRECTVConnectionError(
                "Timeout occurred while connecting to receiver"
            ) from exception
        except (aiohttp.ClientError, SocketGIAEroor) as exception:
            raise DIRECTVConnectionError(
                "Error occurred while communicating with receiver"
            ) from exception

        if response.status == 403:
            raise DIRECTVAccessRestricted(
                "Access restricted. Please ensure external device access is allowed",
                {},
            )

        content_type = response.headers.get("Content-Type")

        if (response.status // 100) in [4, 5]:
            content = await response.read()
            response.close()

            if content_type == "application/json":
                raise DIRECTVError(
                    f"HTTP {response.status}", json.loads(content.decode("utf8"))
                )

            raise DIRECTVError(
                f"HTTP {response.status}",
                {
                    "content-type": content_type,
                    "message": content.decode("utf8"),
                    "status-code": response.status,
                },
            )

        if "application/json" in content_type:
            data = await response.json()
            return data

        return await response.text()

    @property
    def device(self) -> Optional[Device]:
        """Return the cached Device object."""
        return self._device

    async def update(self, full_update: bool = False) -> Device:
        """Get all information about the device in a single call."""
        if self._device is None or full_update:
            info = await self._request("info/getVersion")
            if info is None:
                raise DIRECTVError("DirecTV device returned an empty API response")

            locations = await self._request("info/getLocations")
            if locations is None or "locations" not in locations:
                raise DIRECTVError("DirecTV device returned an empty API response")

            self._device = Device({"info": info, "locations": locations["locations"]})
            return self._device

        self._device.update_from_dict({})
        return self._device

    async def remote(self, key: str, client: str = "0") -> None:
        """Emulate pressing a key on the remote.

        Supported keys: power, poweron, poweroff, format,
        pause, rew, replay, stop, advance, ffwd, record,
        play, guide, active, list, exit, back, menu, info,
        up, down, left, right, select, red, green, yellow,
        blue, chanup, chandown, prev, 0, 1, 2, 3, 4, 5,
        6, 7, 8, 9, dash, enter
        """
        if not key.lower() in VALID_REMOTE_KEYS:
            raise DIRECTVError(f"Remote key is invalid: {key}")

        keypress = {
            "key": key,
            "hold": "keyPress",
            "clientAddr": client,
        }

        await self._request("remote/processKey", params=keypress)

    async def state(self, client: str = "0") -> State:
        """Get state of receiver client."""
        authorized = True
        program = None

        try:
            mode = await self._request("info/mode", params={"clientAddr": client})
            available = True
            standby = mode["mode"] == 1
        except DIRECTVAccessRestricted:
            authorized = False
            available = False
            standby = True
        except DIRECTVError:
            available = False
            standby = True

        if not standby:
            try:
                program = await self.tuned(client)
            except DIRECTVAccessRestricted:
                authorized = False
                program = None
            except DIRECTVError:
                available = False
                program = None

        return State(
            authorized=authorized,
            available=available,
            standby=standby,
            program=program,
        )

    async def status(self, client: str = "0") -> str:
        """Get basic status of receiver client."""
        try:
            mode = await self._request("info/mode", params={"clientAddr": client})
            return "standby" if mode["mode"] == 1 else "active"
        except DIRECTVAccessRestricted:
            return "unauthorized"
        except DIRECTVError:
            return "unavailable"

    async def tune(self, channel: str, client: str = "0") -> None:
        """Change the channel on the receiver."""
        major, minor = parse_channel_number(channel)

        tune = {
            "major": major,
            "minor": minor,
            "clientAddr": client,
        }

        await self._request("tv/tune", params=tune)

    async def tuned(self, client: str = "0") -> Program:
        """Get currently tuned program."""
        tuned = await self._request("tv/getTuned", params={"clientAddr": client})
        return Program.from_dict(tuned)

    async def close(self) -> None:
        """Close open client session."""
        if self._session and self._close_session:
            await self._session.close()

    async def __aenter__(self) -> "DIRECTV":
        """Async enter."""
        return self

    async def __aexit__(self, *exc_info) -> None:
        """Async exit."""
        await self.close()