GeekMagicWeatherClockAPI 0.1.0__py3-none-any.whl → 0.1.1__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.
- GeekMagicWeatherClockAPI/core.py +189 -134
- geekmagicweatherclockapi-0.1.1.dist-info/METADATA +253 -0
- geekmagicweatherclockapi-0.1.1.dist-info/RECORD +7 -0
- geekmagicweatherclockapi-0.1.0.dist-info/METADATA +0 -125
- geekmagicweatherclockapi-0.1.0.dist-info/RECORD +0 -7
- {geekmagicweatherclockapi-0.1.0.dist-info → geekmagicweatherclockapi-0.1.1.dist-info}/WHEEL +0 -0
- {geekmagicweatherclockapi-0.1.0.dist-info → geekmagicweatherclockapi-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {geekmagicweatherclockapi-0.1.0.dist-info → geekmagicweatherclockapi-0.1.1.dist-info}/top_level.txt +0 -0
GeekMagicWeatherClockAPI/core.py
CHANGED
|
@@ -4,196 +4,251 @@ from urllib.parse import quote
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class SmallTV:
|
|
7
|
-
|
|
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:
|
|
8
33
|
self.ip = ip
|
|
9
34
|
self.base_url = f"http://{ip}"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
18
57
|
}
|
|
19
58
|
|
|
20
|
-
def
|
|
59
|
+
def _get(self, endpoint: str, **kwargs) -> dict:
|
|
21
60
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
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()``.
|
|
24
85
|
"""
|
|
86
|
+
url = f"{self.base_url}{endpoint}"
|
|
87
|
+
kwargs.setdefault("timeout", 30)
|
|
25
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
|
+
"""
|
|
26
120
|
file_path = Path(file_path)
|
|
27
121
|
|
|
28
122
|
if not file_path.exists():
|
|
29
|
-
|
|
123
|
+
return self._build_result(
|
|
124
|
+
success=False,
|
|
125
|
+
error=f"File not found: {file_path}",
|
|
126
|
+
)
|
|
30
127
|
|
|
31
128
|
filename = file_path.name
|
|
32
129
|
|
|
33
|
-
|
|
34
|
-
gif_data =
|
|
130
|
+
try:
|
|
131
|
+
gif_data = file_path.read_bytes()
|
|
132
|
+
except OSError as exc:
|
|
133
|
+
return self._build_result(success=False, error=str(exc))
|
|
35
134
|
|
|
36
|
-
|
|
37
|
-
"X-Requested-With": "XMLHttpRequest"
|
|
38
|
-
}
|
|
135
|
+
last_result: dict = {}
|
|
39
136
|
|
|
40
137
|
for attempt in range(1, retries + 1):
|
|
41
|
-
#
|
|
138
|
+
# Clean up any partial upload from a previous failed attempt.
|
|
42
139
|
if attempt > 1:
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
self.delete(filename)
|
|
46
|
-
except Exception:
|
|
47
|
-
pass
|
|
140
|
+
self.delete(filename)
|
|
48
141
|
|
|
49
142
|
files = {
|
|
50
143
|
"update": (filename, gif_data, "image/gif"),
|
|
51
144
|
"image": (filename, gif_data, "image/gif"),
|
|
52
145
|
}
|
|
53
146
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
f"{self.base_url}/doUpload?dir=/image/",
|
|
57
|
-
files=files,
|
|
58
|
-
headers=headers,
|
|
59
|
-
timeout=30
|
|
60
|
-
)
|
|
61
|
-
print("Upload Status:", r.status_code)
|
|
62
|
-
return True
|
|
63
|
-
|
|
64
|
-
except requests.exceptions.InvalidHeader:
|
|
65
|
-
print(f"Upload failed (malformed headers from SmallTV firmware) — attempt {attempt}/{retries}")
|
|
66
|
-
if attempt == retries:
|
|
67
|
-
try:
|
|
68
|
-
self.delete(filename)
|
|
69
|
-
except Exception:
|
|
70
|
-
pass
|
|
71
|
-
raise RuntimeError(
|
|
72
|
-
f"Upload of '{filename}' failed after {retries} attempts."
|
|
73
|
-
)
|
|
147
|
+
result = self._post("/doUpload?dir=/image/", files=files)
|
|
148
|
+
last_result = result
|
|
74
149
|
|
|
75
|
-
|
|
150
|
+
if result["success"]:
|
|
151
|
+
return result
|
|
76
152
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"""
|
|
153
|
+
# Only retry on malformed-header errors (firmware quirk).
|
|
154
|
+
if result.get("error") and "malformed" not in result["error"].lower():
|
|
155
|
+
return result
|
|
81
156
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
f"{
|
|
86
|
-
|
|
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')}"
|
|
87
162
|
)
|
|
163
|
+
return last_result
|
|
88
164
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return r
|
|
92
|
-
|
|
93
|
-
def set_theme(self, theme):
|
|
165
|
+
def set_image(self, filename: str) -> dict:
|
|
94
166
|
"""
|
|
95
|
-
Set the
|
|
96
|
-
|
|
97
|
-
Accepts:
|
|
98
|
-
1-7
|
|
99
|
-
or
|
|
100
|
-
"Weather Clock Today"
|
|
101
|
-
"Weather Forecast"
|
|
102
|
-
"Photo Album"
|
|
103
|
-
"Time Style 1"
|
|
104
|
-
"Time Style 2"
|
|
105
|
-
"Time Style 3"
|
|
106
|
-
"Simple Weather Clock"
|
|
167
|
+
Set the currently displayed image on the device.
|
|
107
168
|
"""
|
|
169
|
+
encoded = quote(filename)
|
|
170
|
+
return self._get(f"/set?img=/image/{encoded}")
|
|
108
171
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for k, v in self.THEMES.items()
|
|
113
|
-
}
|
|
172
|
+
def set_theme(self, theme) -> dict:
|
|
173
|
+
"""
|
|
174
|
+
Set the active SmallTV theme.
|
|
114
175
|
|
|
115
|
-
|
|
176
|
+
*theme* may be an integer (1–7) or one of the theme name strings:
|
|
116
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())
|
|
117
188
|
if theme_id is None:
|
|
118
|
-
|
|
119
|
-
|
|
189
|
+
return self._build_result(
|
|
190
|
+
success=False,
|
|
191
|
+
error=f"Unknown theme '{theme}'. Valid themes: {list(self.THEMES.values())}",
|
|
120
192
|
)
|
|
121
193
|
else:
|
|
122
|
-
|
|
194
|
+
try:
|
|
195
|
+
theme_id = int(theme)
|
|
196
|
+
except (TypeError, ValueError) as exc:
|
|
197
|
+
return self._build_result(success=False, error=str(exc))
|
|
123
198
|
|
|
124
199
|
if theme_id not in self.THEMES:
|
|
125
|
-
|
|
126
|
-
|
|
200
|
+
return self._build_result(
|
|
201
|
+
success=False,
|
|
202
|
+
error=f"Theme ID must be between 1 and 7, got {theme_id}.",
|
|
127
203
|
)
|
|
128
204
|
|
|
129
|
-
|
|
130
|
-
f"{self.base_url}/set?theme={theme_id}",
|
|
131
|
-
timeout=10
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
print(
|
|
135
|
-
f"Theme set to {theme_id}: "
|
|
136
|
-
f"{self.THEMES[theme_id]}"
|
|
137
|
-
)
|
|
138
|
-
print("Status:", r.status_code)
|
|
205
|
+
return self._get(f"/set?theme={theme_id}")
|
|
139
206
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def set_brightness(self, value):
|
|
207
|
+
def set_brightness(self, value) -> dict:
|
|
143
208
|
"""
|
|
144
|
-
Set
|
|
209
|
+
Set the display brightness (0–100).
|
|
145
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
|
+
)
|
|
146
221
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if value < 0 or value > 100:
|
|
150
|
-
raise ValueError("Brightness must be between 0 and 100")
|
|
151
|
-
|
|
152
|
-
r = requests.get(
|
|
153
|
-
f"{self.base_url}/set?brt={value}",
|
|
154
|
-
timeout=10
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
print(f"Brightness set to {value}")
|
|
158
|
-
print("Status:", r.status_code)
|
|
159
|
-
|
|
160
|
-
return r
|
|
222
|
+
return self._get(f"/set?brt={value}")
|
|
161
223
|
|
|
162
|
-
def delete(self, filename):
|
|
224
|
+
def delete(self, filename: str) -> dict:
|
|
163
225
|
"""
|
|
164
|
-
Delete an image from the
|
|
226
|
+
Delete an image from the device.
|
|
165
227
|
"""
|
|
228
|
+
encoded = quote(filename)
|
|
229
|
+
return self._get(f"/delete?file=/image/{encoded}")
|
|
166
230
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
r = requests.get(
|
|
170
|
-
f"{self.base_url}/delete?file=/image/{encoded_filename}",
|
|
171
|
-
timeout=10
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
print("Delete Status:", r.status_code)
|
|
175
|
-
|
|
176
|
-
return r
|
|
177
|
-
|
|
178
|
-
def upload_and_set(self, file_path):
|
|
231
|
+
def upload_and_set(self, file_path) -> dict:
|
|
179
232
|
"""
|
|
180
233
|
Upload a file and immediately display it.
|
|
181
|
-
"""
|
|
182
234
|
|
|
235
|
+
Returns the result of ``set_image`` on success, or the failed
|
|
236
|
+
``upload`` result if the upload did not succeed.
|
|
237
|
+
"""
|
|
183
238
|
file_path = Path(file_path)
|
|
184
239
|
|
|
185
|
-
self.upload(file_path)
|
|
240
|
+
upload_result = self.upload(file_path)
|
|
241
|
+
if not upload_result["success"]:
|
|
242
|
+
return upload_result
|
|
186
243
|
|
|
187
244
|
return self.set_image(file_path.name)
|
|
188
245
|
|
|
189
|
-
def replace(self, old_filename, new_file):
|
|
246
|
+
def replace(self, old_filename: str, new_file) -> dict:
|
|
190
247
|
"""
|
|
191
|
-
Delete
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
try:
|
|
195
|
-
self.delete(old_filename)
|
|
196
|
-
except Exception:
|
|
197
|
-
pass
|
|
248
|
+
Delete *old_filename*, upload *new_file*, and display it.
|
|
198
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)
|
|
199
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/*`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
GeekMagicWeatherClockAPI/__init__.py,sha256=sIM2Yj7PGpD0r8SrPGvoboz_USBJww7bQB-71qM7ZBc,49
|
|
2
|
+
GeekMagicWeatherClockAPI/core.py,sha256=FsMzJKNj3_r2nyGt82oV2LDKe9tu0w3Rw5Qamitq0Eg,8449
|
|
3
|
+
geekmagicweatherclockapi-0.1.1.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
4
|
+
geekmagicweatherclockapi-0.1.1.dist-info/METADATA,sha256=25p8KkZuC8V6Qvi3GhFQOl0917B6yQQ0IQR3EfxkQwQ,5935
|
|
5
|
+
geekmagicweatherclockapi-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
geekmagicweatherclockapi-0.1.1.dist-info/top_level.txt,sha256=3viXouZ_1SkVcqd-ChcYP3Nj1AoIoa2xFxm_XfsbPGQ,25
|
|
7
|
+
geekmagicweatherclockapi-0.1.1.dist-info/RECORD,,
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: GeekMagicWeatherClockAPI
|
|
3
|
-
Version: 0.1.0
|
|
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 simple 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
|
-
---
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install GeekMagicWeatherClockAPI
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Requirements
|
|
25
|
-
|
|
26
|
-
- Python 3.7+
|
|
27
|
-
- [`requests`](https://pypi.org/project/requests/) (installed automatically)
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Setup
|
|
32
|
-
|
|
33
|
-
Find your SmallTV's local IP address (check your router's device list or the SmallTV's settings menu), then:
|
|
34
|
-
|
|
35
|
-
```python
|
|
36
|
-
from GeekMagicWeatherClockAPI import SmallTV
|
|
37
|
-
|
|
38
|
-
tv = SmallTV("192.168.1.85")
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## Usage
|
|
44
|
-
|
|
45
|
-
### Upload an image
|
|
46
|
-
|
|
47
|
-
```python
|
|
48
|
-
tv.upload("my_animation.gif")
|
|
49
|
-
tv.upload("my_animation.gif", retries=5) # Custom retry count
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Display an image
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
tv.set_image("my_animation.gif")
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Upload and immediately display
|
|
59
|
-
|
|
60
|
-
```python
|
|
61
|
-
tv.upload_and_set("my_animation.gif")
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Replace an image
|
|
65
|
-
|
|
66
|
-
```python
|
|
67
|
-
tv.replace("old_animation.gif", "new_animation.gif")
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Delete an image
|
|
71
|
-
|
|
72
|
-
```python
|
|
73
|
-
tv.delete("my_animation.gif")
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### Set brightness
|
|
77
|
-
|
|
78
|
-
Accepts a value from `0` to `100`.
|
|
79
|
-
|
|
80
|
-
```python
|
|
81
|
-
tv.set_brightness(75)
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Set theme
|
|
85
|
-
|
|
86
|
-
Accepts either an integer (`1`–`7`) or the theme name as a string (case-insensitive).
|
|
87
|
-
|
|
88
|
-
```python
|
|
89
|
-
tv.set_theme(3)
|
|
90
|
-
tv.set_theme("Photo Album")
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
| ID | Theme Name |
|
|
94
|
-
|----|----------------------|
|
|
95
|
-
| 1 | Weather Clock Today |
|
|
96
|
-
| 2 | Weather Forecast |
|
|
97
|
-
| 3 | Photo Album |
|
|
98
|
-
| 4 | Time Style 1 |
|
|
99
|
-
| 5 | Time Style 2 |
|
|
100
|
-
| 6 | Time Style 3 |
|
|
101
|
-
| 7 | Simple Weather Clock |
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Full Example
|
|
106
|
-
|
|
107
|
-
```python
|
|
108
|
-
from GeekMagicWeatherClockAPI import SmallTV
|
|
109
|
-
|
|
110
|
-
tv = SmallTV("192.168.1.85")
|
|
111
|
-
|
|
112
|
-
tv.upload_and_set("spaceman.gif")
|
|
113
|
-
tv.set_brightness(30)
|
|
114
|
-
tv.set_theme("Weather Clock Today")
|
|
115
|
-
tv.replace("spaceman.gif", "new_image.gif")
|
|
116
|
-
tv.delete("new_image.gif")
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## Notes
|
|
122
|
-
|
|
123
|
-
- The SmallTV firmware occasionally returns malformed `Content-Length` headers on upload responses. The `upload` method treats this as a failure and retries automatically.
|
|
124
|
-
- All methods can raise `requests.exceptions.ConnectionError` if the device is unreachable.
|
|
125
|
-
- File management methods operate on the `/image/` directory on the device.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
GeekMagicWeatherClockAPI/__init__.py,sha256=sIM2Yj7PGpD0r8SrPGvoboz_USBJww7bQB-71qM7ZBc,49
|
|
2
|
-
GeekMagicWeatherClockAPI/core.py,sha256=FdqOxePm5qN2xBwQ9iEpfRBtHXj7cJ_rnUvGbmeg7i8,5269
|
|
3
|
-
geekmagicweatherclockapi-0.1.0.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
4
|
-
geekmagicweatherclockapi-0.1.0.dist-info/METADATA,sha256=ZcW-myU1Gjf9_o6svNkow4z3GInKWe-9nr_PEe_fCak,2704
|
|
5
|
-
geekmagicweatherclockapi-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
-
geekmagicweatherclockapi-0.1.0.dist-info/top_level.txt,sha256=3viXouZ_1SkVcqd-ChcYP3Nj1AoIoa2xFxm_XfsbPGQ,25
|
|
7
|
-
geekmagicweatherclockapi-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{geekmagicweatherclockapi-0.1.0.dist-info → geekmagicweatherclockapi-0.1.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|