atvnotif 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,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: atvnotif
3
+ Version: 0.1.0
4
+ Summary: A Python client for sending encrypted notifications to Android TV Notifier
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/atvnotif/atvnotif.py
7
+ Project-URL: Repository, https://github.com/atvnotif/atvnotif.py
8
+ Project-URL: Bug Tracker, https://github.com/atvnotif/atvnotif.py/issues
9
+ Keywords: android-tv,notifications,home-automation,atvnotif
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Home Automation
18
+ Classifier: Topic :: Communications
19
+ Classifier: Framework :: AsyncIO
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: cryptography>=35.0.0
23
+ Requires-Dist: httpx>=0.20.0
24
+ Requires-Dist: zeroconf>=0.33.0
25
+
26
+ # atvnotif
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/atvnotif)](https://pypi.org/project/atvnotif/)
29
+ [![Supported Python Versions](https://img.shields.io/pypi/pyversions/atvnotif)](https://pypi.org/project/atvnotif/)
30
+ [![License](https://img.shields.io/github/license/atvnotif/atvnotif.py)](https://github.com/atvnotif/atvnotif.py/blob/main/LICENSE)
31
+
32
+ A Python client library for sending encrypted notifications to the **Android TV Notifier** app (`com.smrtprjcts.atvnotif`).
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install atvnotif
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ import asyncio
44
+ from atvnotif import ATVNotifier
45
+
46
+ async def main():
47
+ # Replace with your TV's IP address and pairing code (remove hyphens or leave them, the library cleans it)
48
+ notifier = ATVNotifier("192.168.1.100", "1234-5678-ABCD")
49
+
50
+ await notifier.async_notify(
51
+ message="Someone is at the door!",
52
+ title="Doorbell Alert",
53
+ duration=10,
54
+ position=0, # Top-Right
55
+ notif_sound=True
56
+ )
57
+
58
+ if __name__ == "__main__":
59
+ asyncio.run(main())
60
+ ```
@@ -0,0 +1,35 @@
1
+ # atvnotif
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/atvnotif)](https://pypi.org/project/atvnotif/)
4
+ [![Supported Python Versions](https://img.shields.io/pypi/pyversions/atvnotif)](https://pypi.org/project/atvnotif/)
5
+ [![License](https://img.shields.io/github/license/atvnotif/atvnotif.py)](https://github.com/atvnotif/atvnotif.py/blob/main/LICENSE)
6
+
7
+ A Python client library for sending encrypted notifications to the **Android TV Notifier** app (`com.smrtprjcts.atvnotif`).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install atvnotif
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ import asyncio
19
+ from atvnotif import ATVNotifier
20
+
21
+ async def main():
22
+ # Replace with your TV's IP address and pairing code (remove hyphens or leave them, the library cleans it)
23
+ notifier = ATVNotifier("192.168.1.100", "1234-5678-ABCD")
24
+
25
+ await notifier.async_notify(
26
+ message="Someone is at the door!",
27
+ title="Doorbell Alert",
28
+ duration=10,
29
+ position=0, # Top-Right
30
+ notif_sound=True
31
+ )
32
+
33
+ if __name__ == "__main__":
34
+ asyncio.run(main())
35
+ ```
@@ -0,0 +1,5 @@
1
+ from .client import ATVNotifier
2
+ from .discover import discover_devices
3
+ from .qr import decode_qr_image
4
+
5
+ __all__ = ["ATVNotifier", "discover_devices", "decode_qr_image"]
@@ -0,0 +1,149 @@
1
+ import os
2
+ import json
3
+ import httpx
4
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
5
+
6
+ class ATVNotifier:
7
+ """Client class to communicate and send encrypted notifications to Android TV Notifier."""
8
+
9
+ def __init__(self, host: str, pairing_code: str, port: int = 7878):
10
+ self.host = host
11
+ self.port = port
12
+ self.pairing_code = pairing_code
13
+ self.key = self._derive_key(pairing_code)
14
+
15
+ def _derive_key(self, pairing_code: str) -> bytes:
16
+ """Derives the 32-byte AES key from the pairing code by reversing and repeating it."""
17
+ cleaned = pairing_code.replace("-", "")
18
+ reversed_code = cleaned[::-1]
19
+ padded = (reversed_code * 32)[:32]
20
+ return padded.encode("utf-8")
21
+
22
+ def encrypt_payload(self, data: dict) -> bytes:
23
+ """Encrypts the JSON payload using AES-256-GCM with a random 12-byte IV."""
24
+ json_payload = json.dumps(data)
25
+ return self.encrypt_raw_string(json_payload)
26
+
27
+ def encrypt_raw_string(self, text: str) -> bytes:
28
+ """Encrypts a raw string using AES-256-GCM with a random 12-byte IV."""
29
+ aesgcm = AESGCM(self.key)
30
+ iv = os.urandom(12)
31
+ ciphertext = aesgcm.encrypt(iv, text.encode("utf-8"), None)
32
+ return iv + ciphertext
33
+
34
+ def decrypt_payload(self, encrypted_data: bytes) -> str:
35
+ """Decrypts a response payload using AES-256-GCM."""
36
+ if len(encrypted_data) < 28:
37
+ raise ValueError("Encrypted data too short")
38
+ iv = encrypted_data[:12]
39
+ ciphertext = encrypted_data[12:]
40
+ aesgcm = AESGCM(self.key)
41
+ decrypted = aesgcm.decrypt(iv, ciphertext, None)
42
+ return decrypted.decode("utf-8")
43
+
44
+ async def async_get_apps(self) -> list[str]:
45
+ """Retrieves a list of installed apps on the TV."""
46
+ url = f"http://{self.host}:{self.port}/apps"
47
+ encrypted_body = self.encrypt_payload({})
48
+ async with httpx.AsyncClient() as client:
49
+ response = await client.post(
50
+ url,
51
+ content=encrypted_body,
52
+ headers={"Content-Type": "application/octet-stream"},
53
+ timeout=10.0,
54
+ )
55
+ response.raise_for_status()
56
+ decrypted_resp = self.decrypt_payload(response.content)
57
+ return json.loads(decrypted_resp)
58
+
59
+ async def async_get_info(self) -> str:
60
+ """Retrieves TV info (usually the device name)."""
61
+ url = f"http://{self.host}:{self.port}/info"
62
+ encrypted_body = self.encrypt_payload({})
63
+ async with httpx.AsyncClient() as client:
64
+ response = await client.post(
65
+ url,
66
+ content=encrypted_body,
67
+ headers={"Content-Type": "application/octet-stream"},
68
+ timeout=10.0,
69
+ )
70
+ response.raise_for_status()
71
+ return response.text
72
+
73
+ async def async_open_app(self, package_name: str) -> bool:
74
+ """Opens an application on the TV by package name."""
75
+ url = f"http://{self.host}:{self.port}/open"
76
+ encrypted_body = self.encrypt_raw_string(package_name)
77
+ async with httpx.AsyncClient() as client:
78
+ response = await client.post(
79
+ url,
80
+ content=encrypted_body,
81
+ headers={"Content-Type": "application/octet-stream"},
82
+ timeout=10.0,
83
+ )
84
+ response.raise_for_status()
85
+ return response.text.strip().lower() == "true"
86
+
87
+
88
+ async def async_notify(
89
+ self,
90
+ message: str,
91
+ title: str = None,
92
+ sender: str = "Home Assistant",
93
+ duration: int = 5,
94
+ position: int = 0,
95
+ priority: int = 1,
96
+ bg_color: int = None,
97
+ title_color: int = None,
98
+ msg_color: int = None,
99
+ title_size: float = None,
100
+ msg_size: float = None,
101
+ icon: str = None,
102
+ small_icon: str = None,
103
+ big_image: str = None,
104
+ interact: bool = False,
105
+ notif_sound: bool = True,
106
+ wakeup: bool = True,
107
+ ) -> httpx.Response:
108
+ """Sends an encrypted notification payload asynchronously to the TV."""
109
+ payload = {
110
+ "msg": message,
111
+ "src": sender,
112
+ "duration": duration,
113
+ "position": position,
114
+ "prio": priority,
115
+ "interact": interact,
116
+ "notifSound": notif_sound,
117
+ "wakeup": wakeup,
118
+ }
119
+ if title:
120
+ payload["title"] = title
121
+ if bg_color is not None:
122
+ payload["bgColor"] = bg_color
123
+ if title_color is not None:
124
+ payload["titleColor"] = title_color
125
+ if msg_color is not None:
126
+ payload["msgColor"] = msg_color
127
+ if title_size is not None:
128
+ payload["titleSize"] = title_size
129
+ if msg_size is not None:
130
+ payload["msgSize"] = msg_size
131
+ if icon:
132
+ payload["icon"] = icon
133
+ if small_icon:
134
+ payload["smallIcon"] = small_icon
135
+ if big_image:
136
+ payload["bigImg"] = big_image
137
+
138
+ encrypted_body = self.encrypt_payload(payload)
139
+ url = f"http://{self.host}:{self.port}/notify"
140
+
141
+ async with httpx.AsyncClient() as client:
142
+ response = await client.post(
143
+ url,
144
+ content=encrypted_body,
145
+ headers={"Content-Type": "application/octet-stream"},
146
+ timeout=10.0,
147
+ )
148
+ response.raise_for_status()
149
+ return response
@@ -0,0 +1,90 @@
1
+ import time
2
+ import uuid
3
+ import logging
4
+
5
+ _LOGGER = logging.getLogger(__name__)
6
+
7
+ def decode_base58_uuid(s: str) -> str:
8
+ """Decodes a Base58 string into a standard UUID string."""
9
+ alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
10
+ try:
11
+ num = 0
12
+ for char in s:
13
+ num = num * 58 + alphabet.index(char)
14
+ val = num.to_bytes(16, 'big')
15
+ return str(uuid.UUID(bytes=val))
16
+ except Exception as e:
17
+ _LOGGER.debug("Failed to decode base58 UUID from %s: %s", s, e)
18
+ return None
19
+
20
+ def discover_devices(timeout: float = 5.0) -> list[dict]:
21
+ """Scans the network for ATV Notifier devices using zeroconf.
22
+
23
+ Returns a list of dicts, e.g.:
24
+ [
25
+ {
26
+ "name": "Vodafone Vodafone TV 3",
27
+ "host": "192.168.2.104",
28
+ "port": 7878,
29
+ "pairing_code": "60ce69b0-ced7-4872-ba77-66f8dcaf8ba4"
30
+ }
31
+ ]
32
+ """
33
+ try:
34
+ from zeroconf import Zeroconf, ServiceBrowser
35
+ except ImportError:
36
+ _LOGGER.warning("zeroconf library is not installed. Discovery is disabled.")
37
+ return []
38
+
39
+ found_devices = []
40
+
41
+ class ATVNotifListener:
42
+ def remove_service(self, zc, type_, name):
43
+ pass
44
+
45
+ def update_service(self, zc, type_, name):
46
+ pass
47
+
48
+ def add_service(self, zc, type_, name):
49
+ info = zc.get_service_info(type_, name)
50
+ if not info:
51
+ return
52
+
53
+ # Resolve properties
54
+ props = {}
55
+ for k, v in info.properties.items():
56
+ try:
57
+ # Zeroconf keys/values might be bytes
58
+ key_str = k.decode("utf-8") if isinstance(k, bytes) else str(k)
59
+ val_str = v.decode("utf-8") if isinstance(v, bytes) else str(v)
60
+ props[key_str] = val_str
61
+ except Exception:
62
+ pass
63
+
64
+ name_attr = props.get("n", info.server.split(".")[0])
65
+ base58_uuid = props.get("i")
66
+ pairing_code = decode_base58_uuid(base58_uuid) if base58_uuid else None
67
+
68
+ # Get host address
69
+ host = None
70
+ if info.addresses:
71
+ import socket
72
+ # Convert first address to string
73
+ host = socket.inet_ntoa(info.addresses[0])
74
+
75
+ if host:
76
+ found_devices.append({
77
+ "name": name_attr,
78
+ "host": host,
79
+ "port": info.port or 7878,
80
+ "pairing_code": pairing_code
81
+ })
82
+
83
+ zc = Zeroconf()
84
+ listener = ATVNotifListener()
85
+ browser = ServiceBrowser(zc, "_atvnotif._tcp.local.", listener)
86
+
87
+ time.sleep(timeout)
88
+ zc.close()
89
+
90
+ return found_devices
@@ -0,0 +1,184 @@
1
+ import base64
2
+ import json
3
+ import re
4
+ import io
5
+ import logging
6
+ from .discover import decode_base58_uuid
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+ def parse_qr_json(qr_text: str) -> dict:
11
+ """Decodes and parses the reversed-base64 JSON QR code content."""
12
+ try:
13
+ # First try direct base64 decode of reversed or non-reversed text
14
+ # The TV app reverses bytes and base64 encodes them.
15
+ # When we base64 decode, we get a reversed JSON string.
16
+ # So we try direct base64 decode, then reverse the string.
17
+ for text_candidate in [qr_text, qr_text[::-1]]:
18
+ # Try to base64 decode
19
+ for pad in ['', '=', '==', '===']:
20
+ try:
21
+ decoded_bytes = base64.b64decode(text_candidate + pad)
22
+ # Try both normal and reversed byte order
23
+ for byte_candidate in [decoded_bytes, decoded_bytes[::-1]]:
24
+ try:
25
+ decoded_str = byte_candidate.decode('utf-8')
26
+ # Check if it looks like JSON
27
+ if '{' in decoded_str and '}' in decoded_str:
28
+ # Try parsing
29
+ data = json.loads(decoded_str)
30
+ if "ip" in data or "i" in data or "n" in data:
31
+ # Convert pairing code to UUID format if present
32
+ if "i" in data:
33
+ data["pairing_code"] = decode_base58_uuid(data["i"])
34
+ return data
35
+ except Exception:
36
+ pass
37
+ except Exception:
38
+ pass
39
+ except Exception as e:
40
+ _LOGGER.debug("Failed parsing QR JSON from raw text %s: %s", qr_text, e)
41
+ return None
42
+
43
+ def decode_qr_image(img_input, try_ocr: bool = True) -> dict:
44
+ """Decodes a QR code from a file path, bytes, or file-like object.
45
+
46
+ If QR code decoding fails and try_ocr is True, attempts to extract
47
+ the IP address using OCR.
48
+
49
+ Returns a dict with resolved properties:
50
+ {
51
+ "ip": "192.168.2.104",
52
+ "pairing_code": "60ce69b0-ced7-4872-ba77-66f8dcaf8ba4",
53
+ "name": "Vodafone Vodafone TV 3"
54
+ }
55
+ """
56
+ try:
57
+ import cv2
58
+ import numpy as np
59
+ except ImportError:
60
+ raise ImportError("opencv-python and numpy are required to decode QR codes. Run 'pip install opencv-python numpy'")
61
+
62
+ # Load image from path or bytes
63
+ img = None
64
+ if isinstance(img_input, str):
65
+ if img_input.startswith(("http://", "https://")):
66
+ try:
67
+ import urllib.request
68
+ req = urllib.request.Request(
69
+ img_input,
70
+ headers={"User-Agent": "Mozilla/5.0"}
71
+ )
72
+ with urllib.request.urlopen(req, timeout=10.0) as response:
73
+ img_bytes = response.read()
74
+ nparr = np.frombuffer(img_bytes, np.uint8)
75
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
76
+ except Exception as e:
77
+ raise ValueError(f"Failed to fetch image from URL {img_input}: {e}")
78
+ else:
79
+ img = cv2.imread(img_input)
80
+ elif isinstance(img_input, bytes):
81
+ nparr = np.frombuffer(img_input, np.uint8)
82
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
83
+ elif hasattr(img_input, "read"):
84
+ img_bytes = img_input.read()
85
+ nparr = np.frombuffer(img_bytes, np.uint8)
86
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
87
+
88
+ if img is None:
89
+ raise ValueError("Failed to load image from input source")
90
+
91
+ # Grayscale
92
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
93
+
94
+ # Prepare candidates for QR decoding (original, inverted, adaptive threshold, etc.)
95
+ candidates = [
96
+ gray,
97
+ 255 - gray, # Inverted (since TV app shows white-on-black QR code)
98
+ ]
99
+
100
+ # Try OTSU thresholding
101
+ try:
102
+ _, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
103
+ candidates.append(otsu)
104
+ candidates.append(255 - otsu)
105
+ except Exception:
106
+ pass
107
+
108
+ # Try decoding with OpenCV QRCodeDetector
109
+ detector = cv2.QRCodeDetector()
110
+ for candidate in candidates:
111
+ try:
112
+ data, bbox, _ = detector.detectAndDecode(candidate)
113
+ if data:
114
+ parsed = parse_qr_json(data)
115
+ if parsed:
116
+ return parsed
117
+ except Exception:
118
+ pass
119
+
120
+ # Fallback 1: Try pyzbar if installed
121
+ try:
122
+ from pyzbar.pyzbar import decode as pyzbar_decode
123
+ from PIL import Image
124
+ pil_img = Image.fromarray(gray)
125
+ decoded_objs = pyzbar_decode(pil_img)
126
+ for obj in decoded_objs:
127
+ qr_text = obj.data.decode("utf-8")
128
+ parsed = parse_qr_json(qr_text)
129
+ if parsed:
130
+ return parsed
131
+ except ImportError:
132
+ pass
133
+ except Exception as e:
134
+ _LOGGER.debug("pyzbar decoding failed: %s", e)
135
+
136
+ # Fallback 2: Try calling zbarimg CLI if available
137
+ # Save a temporary inverted/preprocessed version of the image to pass to zbarimg
138
+ import subprocess
139
+ import tempfile
140
+ import os
141
+ for i, candidate in enumerate(candidates):
142
+ try:
143
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
144
+ tmp_path = tmp.name
145
+ cv2.imwrite(tmp_path, candidate)
146
+ try:
147
+ res = subprocess.run(["zbarimg", "-q", tmp_path], capture_output=True, text=True)
148
+ if res.returncode == 0:
149
+ # zbarimg outputs: "QR-Code:<data>"
150
+ output_text = res.stdout.strip()
151
+ if output_text.startswith("QR-Code:"):
152
+ qr_text = output_text[len("QR-Code:"):]
153
+ parsed = parse_qr_json(qr_text)
154
+ if parsed:
155
+ os.remove(tmp_path)
156
+ return parsed
157
+ finally:
158
+ if os.path.exists(tmp_path):
159
+ os.remove(tmp_path)
160
+ except Exception as e:
161
+ _LOGGER.debug("zbarimg subprocess attempt failed for candidate %d: %s", i, e)
162
+
163
+ # OCR Fallback for IP address extraction if QR code decoding failed
164
+ if try_ocr:
165
+ try:
166
+ import pytesseract
167
+ # Try to run OCR on grayscale
168
+ ocr_text = pytesseract.image_to_string(gray)
169
+ # Find IP address pattern
170
+ ip_pattern = re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b')
171
+ match = ip_pattern.search(ocr_text)
172
+ if match:
173
+ return {
174
+ "ip": match.group(0),
175
+ "pairing_code": None,
176
+ "name": None,
177
+ "ocr_extracted": True
178
+ }
179
+ except ImportError:
180
+ _LOGGER.debug("pytesseract is not installed; OCR fallback skipped.")
181
+ except Exception as e:
182
+ _LOGGER.debug("OCR extraction failed: %s", e)
183
+
184
+ raise ValueError("Failed to decode QR code from the image")
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: atvnotif
3
+ Version: 0.1.0
4
+ Summary: A Python client for sending encrypted notifications to Android TV Notifier
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/atvnotif/atvnotif.py
7
+ Project-URL: Repository, https://github.com/atvnotif/atvnotif.py
8
+ Project-URL: Bug Tracker, https://github.com/atvnotif/atvnotif.py/issues
9
+ Keywords: android-tv,notifications,home-automation,atvnotif
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Home Automation
18
+ Classifier: Topic :: Communications
19
+ Classifier: Framework :: AsyncIO
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: cryptography>=35.0.0
23
+ Requires-Dist: httpx>=0.20.0
24
+ Requires-Dist: zeroconf>=0.33.0
25
+
26
+ # atvnotif
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/atvnotif)](https://pypi.org/project/atvnotif/)
29
+ [![Supported Python Versions](https://img.shields.io/pypi/pyversions/atvnotif)](https://pypi.org/project/atvnotif/)
30
+ [![License](https://img.shields.io/github/license/atvnotif/atvnotif.py)](https://github.com/atvnotif/atvnotif.py/blob/main/LICENSE)
31
+
32
+ A Python client library for sending encrypted notifications to the **Android TV Notifier** app (`com.smrtprjcts.atvnotif`).
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install atvnotif
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ import asyncio
44
+ from atvnotif import ATVNotifier
45
+
46
+ async def main():
47
+ # Replace with your TV's IP address and pairing code (remove hyphens or leave them, the library cleans it)
48
+ notifier = ATVNotifier("192.168.1.100", "1234-5678-ABCD")
49
+
50
+ await notifier.async_notify(
51
+ message="Someone is at the door!",
52
+ title="Doorbell Alert",
53
+ duration=10,
54
+ position=0, # Top-Right
55
+ notif_sound=True
56
+ )
57
+
58
+ if __name__ == "__main__":
59
+ asyncio.run(main())
60
+ ```
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ atvnotif/__init__.py
4
+ atvnotif/client.py
5
+ atvnotif/discover.py
6
+ atvnotif/qr.py
7
+ atvnotif.egg-info/PKG-INFO
8
+ atvnotif.egg-info/SOURCES.txt
9
+ atvnotif.egg-info/dependency_links.txt
10
+ atvnotif.egg-info/requires.txt
11
+ atvnotif.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ cryptography>=35.0.0
2
+ httpx>=0.20.0
3
+ zeroconf>=0.33.0
@@ -0,0 +1 @@
1
+ atvnotif
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "atvnotif"
7
+ version = "0.1.0"
8
+ description = "A Python client for sending encrypted notifications to Android TV Notifier"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ keywords = ["android-tv", "notifications", "home-automation", "atvnotif"]
13
+ dependencies = [
14
+ "cryptography>=35.0.0",
15
+ "httpx>=0.20.0",
16
+ "zeroconf>=0.33.0"
17
+ ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Operating System :: OS Independent",
26
+ "Topic :: Home Automation",
27
+ "Topic :: Communications",
28
+ "Framework :: AsyncIO",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/atvnotif/atvnotif.py"
33
+ Repository = "https://github.com/atvnotif/atvnotif.py"
34
+ "Bug Tracker" = "https://github.com/atvnotif/atvnotif.py/issues"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["."]
38
+ include = ["atvnotif*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+