aiotja470-intercom 0.1.0__tar.gz

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,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiotja470_intercom
3
+ Version: 0.1.0
4
+ Summary: Asynchronous Python client for the Hager TJA470 Intercom API
5
+ Author-email: Manuel Klimek <klimek@box4.net>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Framework :: AsyncIO
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: aiohttp>=3.8.0
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest>=7.0; extra == "test"
15
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
16
+
17
+ # aiotja470_intercom
18
+
19
+ Asynchronous Python client for the Hager TJA470 Intercom API. This package is designed to be integrated into Home Assistant.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install aiotja470_intercom
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ import asyncio
31
+ from aiotja470_intercom import TJA470IntercomClient, AiohttpRunner
32
+
33
+ async def main():
34
+ runner = AiohttpRunner()
35
+ client = TJA470IntercomClient(
36
+ host="192.168.1.100",
37
+ username="user",
38
+ password="password",
39
+ runner=runner
40
+ )
41
+
42
+ try:
43
+ # Check manifest
44
+ await client.get_manifest()
45
+
46
+ # Get free devices for pairing
47
+ devices = await client.get_free_devices()
48
+ print(devices)
49
+
50
+ # Retrieve provisioning config
51
+ config = await client.get_provisioning("your-uuid")
52
+ print(config)
53
+
54
+ finally:
55
+ await runner.close()
56
+
57
+ if __name__ == "__main__":
58
+ asyncio.run(main())
59
+ ```
@@ -0,0 +1,43 @@
1
+ # aiotja470_intercom
2
+
3
+ Asynchronous Python client for the Hager TJA470 Intercom API. This package is designed to be integrated into Home Assistant.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install aiotja470_intercom
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import asyncio
15
+ from aiotja470_intercom import TJA470IntercomClient, AiohttpRunner
16
+
17
+ async def main():
18
+ runner = AiohttpRunner()
19
+ client = TJA470IntercomClient(
20
+ host="192.168.1.100",
21
+ username="user",
22
+ password="password",
23
+ runner=runner
24
+ )
25
+
26
+ try:
27
+ # Check manifest
28
+ await client.get_manifest()
29
+
30
+ # Get free devices for pairing
31
+ devices = await client.get_free_devices()
32
+ print(devices)
33
+
34
+ # Retrieve provisioning config
35
+ config = await client.get_provisioning("your-uuid")
36
+ print(config)
37
+
38
+ finally:
39
+ await runner.close()
40
+
41
+ if __name__ == "__main__":
42
+ asyncio.run(main())
43
+ ```
@@ -0,0 +1,24 @@
1
+ from .client import TJA470IntercomClient
2
+ from .exceptions import (
3
+ TJA470AuthError,
4
+ TJA470ConnectionError,
5
+ TJA470Error,
6
+ TJA470ResponseError,
7
+ )
8
+ from .models import CalledElement, FreeDevice, Manifest, ProvisioningInfo, SipInfo
9
+ from .runner import AiohttpRunner, Runner
10
+
11
+ __all__ = [
12
+ "TJA470IntercomClient",
13
+ "TJA470Error",
14
+ "TJA470ConnectionError",
15
+ "TJA470AuthError",
16
+ "TJA470ResponseError",
17
+ "Manifest",
18
+ "FreeDevice",
19
+ "ProvisioningInfo",
20
+ "SipInfo",
21
+ "CalledElement",
22
+ "Runner",
23
+ "AiohttpRunner",
24
+ ]
@@ -0,0 +1,233 @@
1
+ import argparse
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ import uuid
8
+ from typing import Any, Dict
9
+
10
+ from aiotja470_intercom.client import TJA470IntercomClient
11
+ from aiotja470_intercom.runner import AiohttpRunner
12
+ from aiotja470_intercom.exceptions import TJA470AuthError
13
+
14
+ CONFIG_FILE = os.path.expanduser("~/.tja470_config.json")
15
+
16
+ def load_config() -> Dict[str, Any]:
17
+ if not os.path.exists(CONFIG_FILE):
18
+ return {}
19
+ with open(CONFIG_FILE, "r") as f:
20
+ return json.load(f)
21
+
22
+ def save_config(config: Dict[str, Any]) -> None:
23
+ with open(CONFIG_FILE, "w") as f:
24
+ json.dump(config, f, indent=4)
25
+
26
+ class CustomParser(argparse.ArgumentParser):
27
+ def error(self, message):
28
+ sys.stderr.write(f'error: {message}\n\n')
29
+ self.print_help()
30
+ sys.exit(2)
31
+
32
+ async def async_main():
33
+ parser = CustomParser(description="TJA-470 Intercom CLI")
34
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging (prints raw HTTP requests and responses)")
35
+ subparsers = parser.add_subparsers(dest="command", required=True)
36
+
37
+ # `pair` command
38
+ pair_parser = subparsers.add_parser("pair", help="Authenticate and pair a new client UUID")
39
+ pair_parser.add_argument("--host", required=True, help="IP address of the TJA-470")
40
+ pair_parser.add_argument("--username", required=True, help="Username for the TJA-470")
41
+ pair_parser.add_argument("--password", required=True, help="Password for the TJA-470")
42
+ pair_parser.add_argument("--uuid", help="Optional custom UUID. If omitted, a random one is generated.")
43
+
44
+ # `status` command
45
+ status_parser = subparsers.add_parser("status", help="Show current connection status and device information")
46
+
47
+ # `run` command
48
+ run_parser = subparsers.add_parser("run", help="Run commands using the cached session")
49
+ run_parser.add_argument("--open-door", action="store_true", help="Open the door (door ID 1)")
50
+ run_parser.add_argument("--switch-camera", action="store_true", help="Switch the camera feed")
51
+ run_parser.add_argument("--provisioning", action="store_true", help="Print the provisioning info")
52
+
53
+ args = parser.parse_args()
54
+
55
+ if args.debug:
56
+ logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
57
+
58
+ if args.command == "pair":
59
+ await run_pair(args)
60
+ elif args.command == "status":
61
+ await run_status(args)
62
+ elif args.command == "run":
63
+ await run_command(args)
64
+
65
+ async def run_pair(args):
66
+ client_uuid = args.uuid if args.uuid else str(uuid.uuid4())
67
+ print(f"Connecting to TJA470 at {args.host}...")
68
+ runner = AiohttpRunner()
69
+ client = TJA470IntercomClient(
70
+ host=args.host,
71
+ username=args.username,
72
+ password=args.password,
73
+ runner=runner
74
+ )
75
+
76
+ try:
77
+ print("Checking API manifest...")
78
+ await client.get_manifest()
79
+
80
+ print("Fetching free devices...")
81
+ devices = await client.get_free_devices()
82
+
83
+ if not devices:
84
+ print("\n❌ No free devices found!")
85
+ print("Please create/free a mobile client in the TJA-470 UI.")
86
+ sys.exit(1)
87
+
88
+ target_device = devices[0]
89
+
90
+ print(f"\nRegistering UUID '{client_uuid}' to device ID {target_device.id}...")
91
+ await client.set_uid(target_device.id, client_uuid)
92
+ print("UUID registered successfully!")
93
+
94
+ # Save config
95
+ config = {
96
+ "host": args.host,
97
+ "username": args.username,
98
+ "password": args.password,
99
+ "uuid": client_uuid,
100
+ "cookies": client.get_cookies()
101
+ }
102
+ save_config(config)
103
+ print(f"\n✅ Configuration and session cookies saved to {CONFIG_FILE}!")
104
+ print("You can now use `tja470 run ...` commands.")
105
+
106
+ except Exception as e:
107
+ print(f"\n❌ An error occurred: {e}")
108
+ finally:
109
+ await runner.close()
110
+
111
+
112
+ async def run_command(args):
113
+ config = load_config()
114
+ if not config:
115
+ print(f"❌ Configuration not found at {CONFIG_FILE}.")
116
+ print("Please run `tja470 pair` first.")
117
+ sys.exit(1)
118
+
119
+ runner = AiohttpRunner()
120
+ client = TJA470IntercomClient(
121
+ host=config["host"],
122
+ username=config["username"],
123
+ password=config["password"],
124
+ runner=runner
125
+ )
126
+
127
+ # Load cached cookies
128
+ cached_cookies = config.get("cookies", {})
129
+ if cached_cookies:
130
+ client.set_cookies(cached_cookies)
131
+
132
+ try:
133
+ uuid = config["uuid"]
134
+
135
+ # Validate session by getting manifest (this will re-authenticate with basic auth if cookie is expired)
136
+ try:
137
+ await client.get_manifest()
138
+ except TJA470AuthError:
139
+ print("❌ Authentication failed. Your credentials might be invalid.")
140
+ sys.exit(1)
141
+
142
+ # Update cached cookies just in case they were refreshed
143
+ new_cookies = client.get_cookies()
144
+ if new_cookies != cached_cookies:
145
+ config["cookies"] = new_cookies
146
+ save_config(config)
147
+
148
+ if args.provisioning:
149
+ print("\nRetrieving provisioning configuration...")
150
+ prov = await client.get_provisioning(uuid)
151
+ print("\n✅ Provisioning Configuration:")
152
+ print(f" SIP ID: {prov.sip_info.sip_id}")
153
+ print(f" SIP Password: {prov.sip_info.sip_password}")
154
+ print(f" RTSP Video URL: {prov.rtsp_video_url}")
155
+
156
+ if args.open_door:
157
+ print("\n🚪 Opening the door...")
158
+ await client.open_door(door_id=1)
159
+ print("Door opened successfully!")
160
+
161
+ if args.switch_camera:
162
+ print("\n📷 Switching the camera feed...")
163
+ await client.switch_camera(uuid)
164
+ print("Camera switched successfully!")
165
+
166
+ except Exception as e:
167
+ print(f"\n❌ An error occurred: {e}")
168
+ finally:
169
+ await runner.close()
170
+
171
+
172
+ async def run_status(args):
173
+ config = load_config()
174
+ if not config:
175
+ print(f"❌ Configuration not found at {CONFIG_FILE}.")
176
+ print("Please run `tja470 pair` first.")
177
+ sys.exit(1)
178
+
179
+ print("Checking connection to TJA-470...")
180
+ runner = AiohttpRunner()
181
+ client = TJA470IntercomClient(
182
+ host=config["host"],
183
+ username=config["username"],
184
+ password=config["password"],
185
+ runner=runner
186
+ )
187
+
188
+ if config.get("cookies"):
189
+ client.set_cookies(config["cookies"])
190
+
191
+ try:
192
+ # Check authentication
193
+ await client.get_manifest()
194
+
195
+ # Save cookies if updated
196
+ new_cookies = client.get_cookies()
197
+ if new_cookies != config.get("cookies", {}):
198
+ config["cookies"] = new_cookies
199
+ save_config(config)
200
+
201
+ print("\n✅ Authentication successful!")
202
+ print(f"Host: {config['host']}")
203
+ print(f"UUID: {config['uuid']}")
204
+
205
+ print("\nFetching provisioning data...")
206
+ prov = await client.get_provisioning(config["uuid"])
207
+ print("\n📋 Intercom Information:")
208
+ print(f" SIP ID: {prov.sip_info.sip_id}")
209
+ print(f" SIP Password: {'*' * len(prov.sip_info.sip_password)}")
210
+ print(f" RTSP Video URL: {prov.rtsp_video_url}")
211
+
212
+ if prov.called_elements:
213
+ print("\n Extensions (Called Elements):")
214
+ for ext in prov.called_elements:
215
+ print(f" - Name: {ext.name or 'Unknown'} (SIP ID: {ext.sip_id})")
216
+
217
+ except TJA470AuthError:
218
+ print("\n❌ Authentication failed. Your session or credentials might be invalid.")
219
+ print("Try running `tja470 pair` again.")
220
+ except Exception as e:
221
+ print(f"\n❌ An error occurred while fetching status: {e}")
222
+ finally:
223
+ await runner.close()
224
+
225
+
226
+ def main():
227
+ try:
228
+ asyncio.run(async_main())
229
+ except KeyboardInterrupt:
230
+ pass
231
+
232
+ if __name__ == "__main__":
233
+ main()
@@ -0,0 +1,91 @@
1
+ import aiohttp
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from .exceptions import TJA470ResponseError, TJA470AuthError
5
+ from .models import FreeDevice, Manifest, ProvisioningInfo
6
+ from .runner import Runner
7
+
8
+ class TJA470IntercomClient:
9
+ """Client for the Hager TJA470 Intercom API."""
10
+
11
+ def __init__(
12
+ self,
13
+ host: str,
14
+ username: str,
15
+ password: str,
16
+ runner: Runner,
17
+ ) -> None:
18
+ self.host = host
19
+ self._auth = aiohttp.BasicAuth(username, password)
20
+ self._runner = runner
21
+
22
+ @property
23
+ def base_url(self) -> str:
24
+ return f"http://{self.host}/API"
25
+
26
+ def get_cookies(self) -> dict[str, str]:
27
+ """Get the current cookies for the API base URL."""
28
+ return self._runner.get_cookies(self.base_url)
29
+
30
+ def set_cookies(self, cookies: dict[str, str]) -> None:
31
+ """Set cookies for the API base URL."""
32
+ self._runner.set_cookies(self.base_url, cookies)
33
+
34
+ async def _request(self, method: str, url: str, json: Optional[Dict[str, Any]] = None) -> Any:
35
+ try:
36
+ return await self._runner.request(method, url, json=json)
37
+ except TJA470AuthError:
38
+ return await self._runner.request(method, url, auth=self._auth, json=json)
39
+
40
+ async def get_manifest(self) -> Manifest:
41
+ """Verify authentication and retrieve the API manifest."""
42
+ url = f"{self.base_url}/manifest"
43
+ response = await self._request("GET", url)
44
+ if isinstance(response, dict):
45
+ return Manifest(raw_data=response)
46
+ elif isinstance(response, str):
47
+ # Sometimes manifest might just return empty string or non-json if successful
48
+ return Manifest()
49
+ else:
50
+ return Manifest()
51
+
52
+ async def get_free_devices(self) -> List[FreeDevice]:
53
+ """List devices available for pairing."""
54
+ url = f"{self.base_url}/runtime/provisioning/freedevices"
55
+ response = await self._request("GET", url)
56
+
57
+ if not isinstance(response, list):
58
+ raise TJA470ResponseError("Expected a list of free devices")
59
+
60
+ return [FreeDevice.from_dict(item) for item in response]
61
+
62
+ async def set_uid(self, device_id: int, uid: str) -> None:
63
+ """Register the UUID as client to the device."""
64
+ url = f"{self.base_url}/runtime/pairing/setuid"
65
+ payload = {
66
+ "id": device_id,
67
+ "uid": uid,
68
+ "description": ""
69
+ }
70
+ await self._request("POST", url, json=payload)
71
+
72
+ async def get_provisioning(self, uid: str) -> ProvisioningInfo:
73
+ """Retrieve the configuration details for the client."""
74
+ url = f"{self.base_url}/runtime/provisioning"
75
+ payload = {"uid": uid}
76
+ response = await self._request("POST", url, json=payload)
77
+
78
+ if not isinstance(response, dict):
79
+ raise TJA470ResponseError("Expected a dictionary for provisioning info")
80
+
81
+ return ProvisioningInfo.from_dict(response)
82
+
83
+ async def switch_camera(self, uid: str) -> None:
84
+ """Switch the camera between different intercom views."""
85
+ url = f"{self.base_url}/runtime/command/camera/switch/{uid}"
86
+ await self._request("POST", url, json={})
87
+
88
+ async def open_door(self, door_id: int = 1) -> None:
89
+ """Trigger the door release."""
90
+ url = f"{self.base_url}/runtime/command/doorrelease/{door_id}"
91
+ await self._request("POST", url, json={})
@@ -0,0 +1,11 @@
1
+ class TJA470Error(Exception):
2
+ """Base exception for TJA470 errors."""
3
+
4
+ class TJA470ConnectionError(TJA470Error):
5
+ """Exception raised for connection errors."""
6
+
7
+ class TJA470AuthError(TJA470Error):
8
+ """Exception raised for authentication errors."""
9
+
10
+ class TJA470ResponseError(TJA470Error):
11
+ """Exception raised for unexpected responses."""
@@ -0,0 +1,63 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ @dataclass
5
+ class Manifest:
6
+ """Representation of a Manifest response."""
7
+ raw_data: Dict[str, Any] = field(default_factory=dict)
8
+
9
+ @dataclass
10
+ class FreeDevice:
11
+ """Representation of a free device."""
12
+ id: int
13
+ name: Optional[str] = None
14
+ mac: Optional[str] = None
15
+
16
+ @classmethod
17
+ def from_dict(cls, data: Dict[str, Any]) -> "FreeDevice":
18
+ return cls(
19
+ id=data.get("id", -1),
20
+ name=data.get("name"),
21
+ mac=data.get("mac")
22
+ )
23
+
24
+ @dataclass
25
+ class SipInfo:
26
+ """SIP connection details."""
27
+ sip_id: str
28
+ sip_password: str
29
+
30
+ @dataclass
31
+ class CalledElement:
32
+ """A called element, e.g. a station."""
33
+ sip_id: str
34
+ name: Optional[str] = None
35
+
36
+ @dataclass
37
+ class ProvisioningInfo:
38
+ """Provisioning details including SIP info and camera streams."""
39
+ sip_info: SipInfo
40
+ rtsp_video_url: str
41
+ called_elements: List[CalledElement] = field(default_factory=list)
42
+ raw_data: Dict[str, Any] = field(default_factory=dict)
43
+
44
+ @classmethod
45
+ def from_dict(cls, data: Dict[str, Any]) -> "ProvisioningInfo":
46
+ sip_info = SipInfo(
47
+ sip_id=data.get("sipId", ""),
48
+ sip_password=data.get("sipPassword", "")
49
+ )
50
+ called_elements = []
51
+ for element in data.get("calledElements", []):
52
+ called_elements.append(
53
+ CalledElement(
54
+ sip_id=element.get("sipId", ""),
55
+ name=element.get("name")
56
+ )
57
+ )
58
+ return cls(
59
+ sip_info=sip_info,
60
+ rtsp_video_url=data.get("rtspVideoUrl", ""),
61
+ called_elements=called_elements,
62
+ raw_data=data
63
+ )
@@ -0,0 +1,116 @@
1
+ import aiohttp
2
+ import logging
3
+ from typing import Any, Dict, Optional, Protocol, Union
4
+
5
+ from .exceptions import TJA470ConnectionError, TJA470AuthError, TJA470ResponseError
6
+
7
+ _LOGGER = logging.getLogger(__name__)
8
+
9
+ class Runner(Protocol):
10
+ """Protocol for executing HTTP requests."""
11
+
12
+ async def request(
13
+ self,
14
+ method: str,
15
+ url: str,
16
+ auth: Optional[aiohttp.BasicAuth] = None,
17
+ json: Optional[Dict[str, Any]] = None,
18
+ ) -> Union[Dict[str, Any], str, bytes, None]:
19
+ """Execute the HTTP request and return the parsed JSON, text, or bytes."""
20
+ ...
21
+
22
+ def get_cookies(self, url: str) -> Dict[str, str]:
23
+ """Get the cookies currently stored for a specific URL."""
24
+ ...
25
+
26
+ def set_cookies(self, url: str, cookies: Dict[str, str]) -> None:
27
+ """Set cookies for a specific URL."""
28
+ ...
29
+
30
+ async def close(self) -> None:
31
+ """Close the underlying session/resources."""
32
+ ...
33
+
34
+
35
+ class AiohttpRunner(Runner):
36
+ """Runner implementation using aiohttp.ClientSession."""
37
+
38
+ def __init__(self, session: Optional[aiohttp.ClientSession] = None) -> None:
39
+ self._session = session
40
+ self._close_session = False
41
+
42
+ async def _get_session(self) -> aiohttp.ClientSession:
43
+ if self._session is None:
44
+ jar = aiohttp.CookieJar(unsafe=True)
45
+ self._session = aiohttp.ClientSession(cookie_jar=jar)
46
+ self._close_session = True
47
+ return self._session
48
+
49
+ async def request(
50
+ self,
51
+ method: str,
52
+ url: str,
53
+ auth: Optional[aiohttp.BasicAuth] = None,
54
+ json: Optional[Dict[str, Any]] = None,
55
+ ) -> Union[Dict[str, Any], str, bytes, None]:
56
+ session = await self._get_session()
57
+
58
+ _LOGGER.debug(f"Request: {method} {url}")
59
+ if json is not None:
60
+ _LOGGER.debug(f"Request JSON: {json}")
61
+
62
+ from yarl import URL
63
+ req_cookies = session.cookie_jar.filter_cookies(URL(url))
64
+ if req_cookies:
65
+ _LOGGER.debug(f"Sending Cookies: {req_cookies}")
66
+
67
+ try:
68
+ async with session.request(method, url, auth=auth, json=json) as response:
69
+ _LOGGER.debug(f"Response Status: {response.status}")
70
+ _LOGGER.debug(f"Response Headers: {response.headers}")
71
+
72
+ if response.status == 401 or response.status == 403:
73
+ raise TJA470AuthError("Authentication failed")
74
+
75
+ response.raise_for_status()
76
+
77
+ content_type = response.headers.get("Content-Type", "")
78
+ if "application/json" in content_type:
79
+ data = await response.json()
80
+ elif "text/" in content_type:
81
+ data = await response.text()
82
+ else:
83
+ data = await response.read()
84
+
85
+ _LOGGER.debug(f"Response Content: {data}")
86
+ return data
87
+
88
+ except aiohttp.ClientConnectorError as e:
89
+ raise TJA470ConnectionError(f"Connection failed: {e}") from e
90
+ except aiohttp.ClientResponseError as e:
91
+ raise TJA470ResponseError(f"HTTP Error {e.status}: {e.message}") from e
92
+ except Exception as e:
93
+ raise TJA470Error(f"An unexpected error occurred: {e}") from e
94
+
95
+ def get_cookies(self, url: str) -> Dict[str, str]:
96
+ if not self._session:
97
+ return {}
98
+ # We iterate over all cookies in the jar to ensure we don't miss any due to path/domain mismatches
99
+ cookies = {}
100
+ for cookie in self._session.cookie_jar:
101
+ cookies[cookie.key] = cookie.value
102
+ return cookies
103
+
104
+ def set_cookies(self, url: str, cookies: Dict[str, str]) -> None:
105
+ if not self._session:
106
+ # We must instantiate the session first to have a cookie jar
107
+ jar = aiohttp.CookieJar(unsafe=True)
108
+ self._session = aiohttp.ClientSession(cookie_jar=jar)
109
+ self._close_session = True
110
+
111
+ from yarl import URL
112
+ self._session.cookie_jar.update_cookies(cookies, response_url=URL(url))
113
+
114
+ async def close(self) -> None:
115
+ if self._session and self._close_session:
116
+ await self._session.close()
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiotja470_intercom
3
+ Version: 0.1.0
4
+ Summary: Asynchronous Python client for the Hager TJA470 Intercom API
5
+ Author-email: Manuel Klimek <klimek@box4.net>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Framework :: AsyncIO
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: aiohttp>=3.8.0
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest>=7.0; extra == "test"
15
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
16
+
17
+ # aiotja470_intercom
18
+
19
+ Asynchronous Python client for the Hager TJA470 Intercom API. This package is designed to be integrated into Home Assistant.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install aiotja470_intercom
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ import asyncio
31
+ from aiotja470_intercom import TJA470IntercomClient, AiohttpRunner
32
+
33
+ async def main():
34
+ runner = AiohttpRunner()
35
+ client = TJA470IntercomClient(
36
+ host="192.168.1.100",
37
+ username="user",
38
+ password="password",
39
+ runner=runner
40
+ )
41
+
42
+ try:
43
+ # Check manifest
44
+ await client.get_manifest()
45
+
46
+ # Get free devices for pairing
47
+ devices = await client.get_free_devices()
48
+ print(devices)
49
+
50
+ # Retrieve provisioning config
51
+ config = await client.get_provisioning("your-uuid")
52
+ print(config)
53
+
54
+ finally:
55
+ await runner.close()
56
+
57
+ if __name__ == "__main__":
58
+ asyncio.run(main())
59
+ ```
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ aiotja470_intercom/__init__.py
4
+ aiotja470_intercom/cli.py
5
+ aiotja470_intercom/client.py
6
+ aiotja470_intercom/exceptions.py
7
+ aiotja470_intercom/models.py
8
+ aiotja470_intercom/runner.py
9
+ aiotja470_intercom.egg-info/PKG-INFO
10
+ aiotja470_intercom.egg-info/SOURCES.txt
11
+ aiotja470_intercom.egg-info/dependency_links.txt
12
+ aiotja470_intercom.egg-info/entry_points.txt
13
+ aiotja470_intercom.egg-info/requires.txt
14
+ aiotja470_intercom.egg-info/top_level.txt
15
+ tests/test_client.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tja470 = aiotja470_intercom.cli:main
@@ -0,0 +1,5 @@
1
+ aiohttp>=3.8.0
2
+
3
+ [test]
4
+ pytest>=7.0
5
+ pytest-asyncio>=0.21.0
@@ -0,0 +1 @@
1
+ aiotja470_intercom
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aiotja470_intercom"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Manuel Klimek", email="klimek@box4.net" },
10
+ ]
11
+ description = "Asynchronous Python client for the Hager TJA470 Intercom API"
12
+ readme = "README.md"
13
+ requires-python = ">=3.11"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Framework :: AsyncIO",
19
+ ]
20
+ dependencies = [
21
+ "aiohttp>=3.8.0",
22
+ ]
23
+
24
+ [project.scripts]
25
+ tja470 = "aiotja470_intercom.cli:main"
26
+
27
+ [project.optional-dependencies]
28
+ test = [
29
+ "pytest>=7.0",
30
+ "pytest-asyncio>=0.21.0",
31
+ ]
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["aiotja470_intercom*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,134 @@
1
+ import pytest
2
+ import aiohttp
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from aiotja470_intercom.client import TJA470IntercomClient
6
+ from aiotja470_intercom.exceptions import TJA470ResponseError
7
+ from aiotja470_intercom.models import FreeDevice, Manifest, ProvisioningInfo
8
+
9
+ class MockRunner:
10
+ def __init__(self):
11
+ self.requests = []
12
+ self.next_response: Union[Dict[str, Any], str, list, Exception] = {}
13
+ self.cookies = {}
14
+
15
+ async def request(
16
+ self,
17
+ method: str,
18
+ url: str,
19
+ auth: Optional[aiohttp.BasicAuth] = None,
20
+ json: Optional[Dict[str, Any]] = None,
21
+ ) -> Union[Dict[str, Any], str, bytes, list, None]:
22
+ self.requests.append({"method": method, "url": url, "auth": auth, "json": json})
23
+ if isinstance(self.next_response, Exception):
24
+ raise self.next_response
25
+ return self.next_response
26
+
27
+ def get_cookies(self, url: str) -> Dict[str, str]:
28
+ return self.cookies.get(url, {})
29
+
30
+ def set_cookies(self, url: str, cookies: Dict[str, str]) -> None:
31
+ if url not in self.cookies:
32
+ self.cookies[url] = {}
33
+ self.cookies[url].update(cookies)
34
+
35
+ async def close(self) -> None:
36
+ pass
37
+
38
+
39
+ @pytest.fixture
40
+ def runner():
41
+ return MockRunner()
42
+
43
+ @pytest.fixture
44
+ def client(runner):
45
+ return TJA470IntercomClient("127.0.0.1", "testuser", "testpass", runner)
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_get_manifest(client, runner):
49
+ runner.next_response = {"status": "ok"}
50
+ manifest = await client.get_manifest()
51
+
52
+ assert isinstance(manifest, Manifest)
53
+ assert manifest.raw_data == {"status": "ok"}
54
+ assert len(runner.requests) == 1
55
+ assert runner.requests[0]["method"] == "GET"
56
+ assert "manifest" in runner.requests[0]["url"]
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_get_free_devices(client, runner):
60
+ runner.next_response = [
61
+ {"id": 42, "name": "Test Device", "mac": "00:11:22:33:44:55"}
62
+ ]
63
+ devices = await client.get_free_devices()
64
+
65
+ assert len(devices) == 1
66
+ assert isinstance(devices[0], FreeDevice)
67
+ assert devices[0].id == 42
68
+ assert devices[0].name == "Test Device"
69
+ assert devices[0].mac == "00:11:22:33:44:55"
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_get_free_devices_error(client, runner):
73
+ runner.next_response = {"error": "not a list"}
74
+ with pytest.raises(TJA470ResponseError):
75
+ await client.get_free_devices()
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_set_uid(client, runner):
79
+ runner.next_response = ""
80
+ await client.set_uid(42, "test-uuid")
81
+
82
+ assert len(runner.requests) == 1
83
+ req = runner.requests[0]
84
+ assert req["method"] == "POST"
85
+ assert "setuid" in req["url"]
86
+ assert req["json"] == {"id": 42, "uid": "test-uuid", "description": ""}
87
+
88
+ @pytest.mark.asyncio
89
+ async def test_get_provisioning(client, runner):
90
+ runner.next_response = {
91
+ "sipId": "1001",
92
+ "sipPassword": "secretpassword",
93
+ "rtspVideoUrl": "rtsp://127.0.0.1/stream",
94
+ "calledElements": [
95
+ {"sipId": "1002", "name": "Station 1"}
96
+ ]
97
+ }
98
+ config = await client.get_provisioning("test-uuid")
99
+
100
+ assert isinstance(config, ProvisioningInfo)
101
+ assert config.sip_info.sip_id == "1001"
102
+ assert config.sip_info.sip_password == "secretpassword"
103
+ assert config.rtsp_video_url == "rtsp://127.0.0.1/stream"
104
+ assert len(config.called_elements) == 1
105
+ assert config.called_elements[0].sip_id == "1002"
106
+ assert config.called_elements[0].name == "Station 1"
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_get_provisioning_error(client, runner):
110
+ runner.next_response = ["not", "a", "dict"]
111
+ with pytest.raises(TJA470ResponseError):
112
+ await client.get_provisioning("test-uuid")
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_switch_camera(client, runner):
116
+ runner.next_response = ""
117
+ await client.switch_camera("test-uuid")
118
+
119
+ assert len(runner.requests) == 1
120
+ assert "camera/switch/test-uuid" in runner.requests[0]["url"]
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_open_door(client, runner):
124
+ runner.next_response = ""
125
+ await client.open_door(1)
126
+
127
+ assert len(runner.requests) == 1
128
+ assert "doorrelease/1" in runner.requests[0]["url"]
129
+
130
+ def test_cookies(client, runner):
131
+ client.set_cookies({"session_id": "12345"})
132
+ cookies = client.get_cookies()
133
+ assert cookies == {"session_id": "12345"}
134
+