GeekMagicWeatherClockAPI 0.1.0__tar.gz → 0.1.1__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.
- geekmagicweatherclockapi-0.1.1/GeekMagicWeatherClockAPI/core.py +254 -0
- geekmagicweatherclockapi-0.1.1/GeekMagicWeatherClockAPI.egg-info/PKG-INFO +253 -0
- geekmagicweatherclockapi-0.1.1/PKG-INFO +253 -0
- geekmagicweatherclockapi-0.1.1/README.md +242 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/pyproject.toml +1 -1
- geekmagicweatherclockapi-0.1.0/GeekMagicWeatherClockAPI/core.py +0 -199
- geekmagicweatherclockapi-0.1.0/GeekMagicWeatherClockAPI.egg-info/PKG-INFO +0 -125
- geekmagicweatherclockapi-0.1.0/PKG-INFO +0 -125
- geekmagicweatherclockapi-0.1.0/README.md +0 -114
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/GeekMagicWeatherClockAPI/__init__.py +0 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/GeekMagicWeatherClockAPI.egg-info/SOURCES.txt +0 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/GeekMagicWeatherClockAPI.egg-info/dependency_links.txt +0 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/GeekMagicWeatherClockAPI.egg-info/requires.txt +0 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/GeekMagicWeatherClockAPI.egg-info/top_level.txt +0 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/LICENSE +0 -0
- {geekmagicweatherclockapi-0.1.0 → geekmagicweatherclockapi-0.1.1}/setup.cfg +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SmallTV:
|
|
7
|
+
"""
|
|
8
|
+
HTTP client for controlling a GeekMagic SmallTV device.
|
|
9
|
+
|
|
10
|
+
All public methods return a standardized response dictionary::
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
"success": bool,
|
|
14
|
+
"status_code": int | None,
|
|
15
|
+
"response": str | None,
|
|
16
|
+
"error": str | None,
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
THEMES: dict[int, str] = {
|
|
21
|
+
1: "Weather Clock Today",
|
|
22
|
+
2: "Weather Forecast",
|
|
23
|
+
3: "Photo Album",
|
|
24
|
+
4: "Time Style 1",
|
|
25
|
+
5: "Time Style 2",
|
|
26
|
+
6: "Time Style 3",
|
|
27
|
+
7: "Simple Weather Clock",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_THEME_LOOKUP: dict[str, int] = {} # populated lazily in __init__
|
|
31
|
+
|
|
32
|
+
def __init__(self, ip: str) -> None:
|
|
33
|
+
self.ip = ip
|
|
34
|
+
self.base_url = f"http://{ip}"
|
|
35
|
+
|
|
36
|
+
self._THEME_LOOKUP = {v.lower(): k for k, v in self.THEMES.items()}
|
|
37
|
+
|
|
38
|
+
self.session = requests.Session()
|
|
39
|
+
self.session.headers.update({"X-Requested-With": "XMLHttpRequest"})
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Internal helpers
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _build_result(
|
|
47
|
+
success: bool,
|
|
48
|
+
status_code: int | None = None,
|
|
49
|
+
response: str | None = None,
|
|
50
|
+
error: str | None = None,
|
|
51
|
+
) -> dict:
|
|
52
|
+
return {
|
|
53
|
+
"success": success,
|
|
54
|
+
"status_code": status_code,
|
|
55
|
+
"response": response,
|
|
56
|
+
"error": error,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def _get(self, endpoint: str, **kwargs) -> dict:
|
|
60
|
+
"""
|
|
61
|
+
Perform a GET request against *endpoint* (relative path + query string).
|
|
62
|
+
|
|
63
|
+
Extra *kwargs* are forwarded to ``session.get()``.
|
|
64
|
+
"""
|
|
65
|
+
url = f"{self.base_url}{endpoint}"
|
|
66
|
+
kwargs.setdefault("timeout", 10)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
r = self.session.get(url, **kwargs)
|
|
70
|
+
return self._build_result(
|
|
71
|
+
success=r.ok,
|
|
72
|
+
status_code=r.status_code,
|
|
73
|
+
response=r.text,
|
|
74
|
+
)
|
|
75
|
+
except requests.RequestException as exc:
|
|
76
|
+
return self._build_result(success=False, error=str(exc))
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
return self._build_result(success=False, error=f"Unexpected error: {exc}")
|
|
79
|
+
|
|
80
|
+
def _post(self, endpoint: str, **kwargs) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Perform a POST request against *endpoint* (relative path + query string).
|
|
83
|
+
|
|
84
|
+
Extra *kwargs* are forwarded to ``session.post()``.
|
|
85
|
+
"""
|
|
86
|
+
url = f"{self.base_url}{endpoint}"
|
|
87
|
+
kwargs.setdefault("timeout", 30)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
r = self.session.post(url, **kwargs)
|
|
91
|
+
return self._build_result(
|
|
92
|
+
success=r.ok,
|
|
93
|
+
status_code=r.status_code,
|
|
94
|
+
response=r.text,
|
|
95
|
+
)
|
|
96
|
+
except requests.exceptions.InvalidHeader as exc:
|
|
97
|
+
# SmallTV firmware occasionally returns malformed response headers.
|
|
98
|
+
return self._build_result(
|
|
99
|
+
success=False,
|
|
100
|
+
error=f"Malformed response headers from device: {exc}",
|
|
101
|
+
)
|
|
102
|
+
except requests.RequestException as exc:
|
|
103
|
+
return self._build_result(success=False, error=str(exc))
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
return self._build_result(success=False, error=f"Unexpected error: {exc}")
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Public API
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def upload(self, file_path, retries: int = 3) -> dict:
|
|
112
|
+
"""
|
|
113
|
+
Upload a GIF/image to the SmallTV.
|
|
114
|
+
|
|
115
|
+
Retries up to *retries* times to work around malformed-header
|
|
116
|
+
responses emitted by some SmallTV firmware versions.
|
|
117
|
+
|
|
118
|
+
Returns a standardized result dict.
|
|
119
|
+
"""
|
|
120
|
+
file_path = Path(file_path)
|
|
121
|
+
|
|
122
|
+
if not file_path.exists():
|
|
123
|
+
return self._build_result(
|
|
124
|
+
success=False,
|
|
125
|
+
error=f"File not found: {file_path}",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
filename = file_path.name
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
gif_data = file_path.read_bytes()
|
|
132
|
+
except OSError as exc:
|
|
133
|
+
return self._build_result(success=False, error=str(exc))
|
|
134
|
+
|
|
135
|
+
last_result: dict = {}
|
|
136
|
+
|
|
137
|
+
for attempt in range(1, retries + 1):
|
|
138
|
+
# Clean up any partial upload from a previous failed attempt.
|
|
139
|
+
if attempt > 1:
|
|
140
|
+
self.delete(filename)
|
|
141
|
+
|
|
142
|
+
files = {
|
|
143
|
+
"update": (filename, gif_data, "image/gif"),
|
|
144
|
+
"image": (filename, gif_data, "image/gif"),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result = self._post("/doUpload?dir=/image/", files=files)
|
|
148
|
+
last_result = result
|
|
149
|
+
|
|
150
|
+
if result["success"]:
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
# Only retry on malformed-header errors (firmware quirk).
|
|
154
|
+
if result.get("error") and "malformed" not in result["error"].lower():
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
# All attempts exhausted — clean up and surface the last error.
|
|
158
|
+
self.delete(filename)
|
|
159
|
+
last_result["error"] = (
|
|
160
|
+
f"Upload of '{filename}' failed after {retries} attempt(s). "
|
|
161
|
+
f"Last error: {last_result.get('error')}"
|
|
162
|
+
)
|
|
163
|
+
return last_result
|
|
164
|
+
|
|
165
|
+
def set_image(self, filename: str) -> dict:
|
|
166
|
+
"""
|
|
167
|
+
Set the currently displayed image on the device.
|
|
168
|
+
"""
|
|
169
|
+
encoded = quote(filename)
|
|
170
|
+
return self._get(f"/set?img=/image/{encoded}")
|
|
171
|
+
|
|
172
|
+
def set_theme(self, theme) -> dict:
|
|
173
|
+
"""
|
|
174
|
+
Set the active SmallTV theme.
|
|
175
|
+
|
|
176
|
+
*theme* may be an integer (1–7) or one of the theme name strings:
|
|
177
|
+
|
|
178
|
+
* ``"Weather Clock Today"``
|
|
179
|
+
* ``"Weather Forecast"``
|
|
180
|
+
* ``"Photo Album"``
|
|
181
|
+
* ``"Time Style 1"``
|
|
182
|
+
* ``"Time Style 2"``
|
|
183
|
+
* ``"Time Style 3"``
|
|
184
|
+
* ``"Simple Weather Clock"``
|
|
185
|
+
"""
|
|
186
|
+
if isinstance(theme, str):
|
|
187
|
+
theme_id = self._THEME_LOOKUP.get(theme.lower())
|
|
188
|
+
if theme_id is None:
|
|
189
|
+
return self._build_result(
|
|
190
|
+
success=False,
|
|
191
|
+
error=f"Unknown theme '{theme}'. Valid themes: {list(self.THEMES.values())}",
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
try:
|
|
195
|
+
theme_id = int(theme)
|
|
196
|
+
except (TypeError, ValueError) as exc:
|
|
197
|
+
return self._build_result(success=False, error=str(exc))
|
|
198
|
+
|
|
199
|
+
if theme_id not in self.THEMES:
|
|
200
|
+
return self._build_result(
|
|
201
|
+
success=False,
|
|
202
|
+
error=f"Theme ID must be between 1 and 7, got {theme_id}.",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return self._get(f"/set?theme={theme_id}")
|
|
206
|
+
|
|
207
|
+
def set_brightness(self, value) -> dict:
|
|
208
|
+
"""
|
|
209
|
+
Set the display brightness (0–100).
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
value = int(value)
|
|
213
|
+
except (TypeError, ValueError) as exc:
|
|
214
|
+
return self._build_result(success=False, error=str(exc))
|
|
215
|
+
|
|
216
|
+
if not 0 <= value <= 100:
|
|
217
|
+
return self._build_result(
|
|
218
|
+
success=False,
|
|
219
|
+
error=f"Brightness must be between 0 and 100, got {value}.",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return self._get(f"/set?brt={value}")
|
|
223
|
+
|
|
224
|
+
def delete(self, filename: str) -> dict:
|
|
225
|
+
"""
|
|
226
|
+
Delete an image from the device.
|
|
227
|
+
"""
|
|
228
|
+
encoded = quote(filename)
|
|
229
|
+
return self._get(f"/delete?file=/image/{encoded}")
|
|
230
|
+
|
|
231
|
+
def upload_and_set(self, file_path) -> dict:
|
|
232
|
+
"""
|
|
233
|
+
Upload a file and immediately display it.
|
|
234
|
+
|
|
235
|
+
Returns the result of ``set_image`` on success, or the failed
|
|
236
|
+
``upload`` result if the upload did not succeed.
|
|
237
|
+
"""
|
|
238
|
+
file_path = Path(file_path)
|
|
239
|
+
|
|
240
|
+
upload_result = self.upload(file_path)
|
|
241
|
+
if not upload_result["success"]:
|
|
242
|
+
return upload_result
|
|
243
|
+
|
|
244
|
+
return self.set_image(file_path.name)
|
|
245
|
+
|
|
246
|
+
def replace(self, old_filename: str, new_file) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
Delete *old_filename*, upload *new_file*, and display it.
|
|
249
|
+
|
|
250
|
+
The delete step is best-effort; failure does not abort the operation.
|
|
251
|
+
Returns the result of ``upload_and_set``.
|
|
252
|
+
"""
|
|
253
|
+
self.delete(old_filename)
|
|
254
|
+
return self.upload_and_set(new_file)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: GeekMagicWeatherClockAPI
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python wrapper for controlling the GeekMagic SmallTV
|
|
5
|
+
Project-URL: Homepage, https://github.com/eman225511/GeekMagicWeatherClockAPI
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: requests
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# GeekMagicWeatherClockAPI
|
|
13
|
+
|
|
14
|
+
A Python wrapper for controlling the **GeekMagic SmallTV** over your local network. Supports uploading images/GIFs, switching themes, adjusting brightness, and managing files on the device.
|
|
15
|
+
|
|
16
|
+
[PyPI](https://pypi.org/project/GeekMagicWeatherClockAPI/)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install GeekMagicWeatherClockAPI
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Python 3.10+
|
|
29
|
+
- [`requests`](https://pypi.org/project/requests/) (installed automatically)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Find your SmallTV's local IP address (check your router's device list or the SmallTV's settings menu), then:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from GeekMagicWeatherClockAPI import SmallTV
|
|
39
|
+
|
|
40
|
+
tv = SmallTV("192.168.1.85")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A persistent HTTP session is created automatically. All requests reuse this session, so there's no need to manage connections manually.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Return values
|
|
48
|
+
|
|
49
|
+
Every method returns the same dictionary — no exceptions are raised for network or validation errors:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
{
|
|
53
|
+
"success": bool, # True if the device responded successfully
|
|
54
|
+
"status_code": int | None, # HTTP status code, or None if the request never completed
|
|
55
|
+
"response": str | None, # Raw response body from the device
|
|
56
|
+
"error": str | None, # Human-readable error message on failure, else None
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Checking results
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
result = tv.set_brightness(50)
|
|
64
|
+
|
|
65
|
+
if result["success"]:
|
|
66
|
+
print("Done!")
|
|
67
|
+
else:
|
|
68
|
+
print("Something went wrong:", result["error"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
### Upload an image
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
tv.upload("my_animation.gif")
|
|
79
|
+
|
|
80
|
+
# Increase retries for flaky firmware responses
|
|
81
|
+
tv.upload("my_animation.gif", retries=5)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Display an image
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
tv.set_image("my_animation.gif")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Upload and immediately display
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
tv.upload_and_set("my_animation.gif")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Replace an image
|
|
97
|
+
|
|
98
|
+
Deletes the old file, uploads the new one, and displays it — all in one call.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
tv.replace("old_animation.gif", "new_animation.gif")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Delete an image
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
tv.delete("my_animation.gif")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Set brightness
|
|
111
|
+
|
|
112
|
+
Accepts a value from `0` to `100`.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
tv.set_brightness(75)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Set theme
|
|
119
|
+
|
|
120
|
+
Accepts either an integer (`1`–`7`) or the theme name as a string (case-insensitive).
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
tv.set_theme(3)
|
|
124
|
+
tv.set_theme("Photo Album")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| ID | Theme Name |
|
|
128
|
+
|----|----------------------|
|
|
129
|
+
| 1 | Weather Clock Today |
|
|
130
|
+
| 2 | Weather Forecast |
|
|
131
|
+
| 3 | Photo Album |
|
|
132
|
+
| 4 | Time Style 1 |
|
|
133
|
+
| 5 | Time Style 2 |
|
|
134
|
+
| 6 | Time Style 3 |
|
|
135
|
+
| 7 | Simple Weather Clock |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
### Basic setup and display
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from GeekMagicWeatherClockAPI import SmallTV
|
|
145
|
+
|
|
146
|
+
tv = SmallTV("192.168.1.85")
|
|
147
|
+
|
|
148
|
+
tv.upload_and_set("spaceman.gif")
|
|
149
|
+
tv.set_brightness(30)
|
|
150
|
+
tv.set_theme("Weather Clock Today")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Checking every result
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
tv = SmallTV("192.168.1.85")
|
|
157
|
+
|
|
158
|
+
for step, result in [
|
|
159
|
+
("upload", tv.upload("clock.gif")),
|
|
160
|
+
("display", tv.set_image("clock.gif")),
|
|
161
|
+
("brightness", tv.set_brightness(60)),
|
|
162
|
+
("theme", tv.set_theme(1)),
|
|
163
|
+
]:
|
|
164
|
+
status = "OK" if result["success"] else f"FAILED — {result['error']}"
|
|
165
|
+
print(f"{step:12} {status}")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Batch upload a folder of GIFs
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
|
|
173
|
+
tv = SmallTV("192.168.1.85")
|
|
174
|
+
|
|
175
|
+
gifs = list(Path("./animations").glob("*.gif"))
|
|
176
|
+
|
|
177
|
+
for gif in gifs:
|
|
178
|
+
result = tv.upload(gif)
|
|
179
|
+
if result["success"]:
|
|
180
|
+
print(f"Uploaded: {gif.name}")
|
|
181
|
+
else:
|
|
182
|
+
print(f"Failed: {gif.name} — {result['error']}")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Rotate through themes on a schedule
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import time
|
|
189
|
+
|
|
190
|
+
tv = SmallTV("192.168.1.85")
|
|
191
|
+
|
|
192
|
+
themes = list(tv.THEMES.keys()) # [1, 2, 3, 4, 5, 6, 7]
|
|
193
|
+
|
|
194
|
+
for theme_id in themes:
|
|
195
|
+
result = tv.set_theme(theme_id)
|
|
196
|
+
if result["success"]:
|
|
197
|
+
print(f"Now showing: {tv.THEMES[theme_id]}")
|
|
198
|
+
time.sleep(10)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Night mode — dim at a set hour
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from datetime import datetime
|
|
205
|
+
|
|
206
|
+
tv = SmallTV("192.168.1.85")
|
|
207
|
+
|
|
208
|
+
hour = datetime.now().hour
|
|
209
|
+
brightness = 10 if 22 <= hour or hour < 7 else 80
|
|
210
|
+
|
|
211
|
+
result = tv.set_brightness(brightness)
|
|
212
|
+
print(f"Brightness set to {brightness}" if result["success"] else result["error"])
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Upload with retry and graceful fallback
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
tv = SmallTV("192.168.1.85")
|
|
219
|
+
|
|
220
|
+
result = tv.upload("hero.gif", retries=5)
|
|
221
|
+
|
|
222
|
+
if result["success"]:
|
|
223
|
+
tv.set_image("hero.gif")
|
|
224
|
+
else:
|
|
225
|
+
print("Upload failed after all retries:", result["error"])
|
|
226
|
+
# Fall back to a theme instead
|
|
227
|
+
tv.set_theme("Weather Clock Today")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Swap a live image safely
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
tv = SmallTV("192.168.1.85")
|
|
234
|
+
|
|
235
|
+
# Removes "old.gif", uploads "new.gif", and displays it
|
|
236
|
+
result = tv.replace("old.gif", "new.gif")
|
|
237
|
+
|
|
238
|
+
if not result["success"]:
|
|
239
|
+
print("Replace failed:", result["error"])
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Notes
|
|
245
|
+
|
|
246
|
+
- The SmallTV firmware occasionally returns malformed `Content-Length` headers on upload responses. The `upload` method automatically retries and cleans up partial uploads when this happens.
|
|
247
|
+
- All methods return a structured dict and never raise — check `result["success"]` instead of wrapping calls in `try/except`.
|
|
248
|
+
- File management methods operate on the `/image/` directory on the device.
|
|
249
|
+
- The `THEMES` dict is a public class attribute and can be read directly to enumerate valid theme names: `SmallTV.THEMES`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
> **Dev note:** on release, bump the version in `pyproject.toml` and run `python -m build` then `twine upload dist/*`.
|