busylib 0.0.2__py3-none-any.whl → 0.2.0__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.
Potentially problematic release.
This version of busylib might be problematic. Click here for more details.
- busylib/__init__.py +1 -82
- busylib/client.py +362 -100
- busylib/exceptions.py +5 -0
- busylib/types.py +205 -4
- busylib-0.2.0.dist-info/METADATA +228 -0
- busylib-0.2.0.dist-info/RECORD +9 -0
- busylib-0.0.2.dist-info/METADATA +0 -162
- busylib-0.0.2.dist-info/RECORD +0 -8
- {busylib-0.0.2.dist-info → busylib-0.2.0.dist-info}/WHEEL +0 -0
- {busylib-0.0.2.dist-info → busylib-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {busylib-0.0.2.dist-info → busylib-0.2.0.dist-info}/top_level.txt +0 -0
busylib/__init__.py
CHANGED
|
@@ -1,82 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Main library for interacting with the Busy Bar API.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import IO
|
|
6
|
-
|
|
7
|
-
from busylib.client import ApiClient
|
|
8
|
-
from busylib.types import ApiResponse
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class BusyBar:
|
|
12
|
-
"""
|
|
13
|
-
Main library class for interacting with the Busy Bar API.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __init__(self, addr: str = "10.0.4.20"):
|
|
17
|
-
"""
|
|
18
|
-
Creates an instance of BUSY Bar.
|
|
19
|
-
Initializes the API client with the provided address.
|
|
20
|
-
|
|
21
|
-
:param addr: The address of the device.
|
|
22
|
-
"""
|
|
23
|
-
self._addr = addr
|
|
24
|
-
self._client = ApiClient(f"http://{self._addr}/api/")
|
|
25
|
-
|
|
26
|
-
def upload_asset(
|
|
27
|
-
self, app_id: str, file_name: str, file: bytes | IO[bytes]
|
|
28
|
-
) -> ApiResponse | None:
|
|
29
|
-
"""
|
|
30
|
-
Uploads an asset to the device.
|
|
31
|
-
|
|
32
|
-
:param app_id: Application ID for organizing assets.
|
|
33
|
-
:param file_name: Filename for the uploaded asset.
|
|
34
|
-
:param file: File data to upload (bytes or file-like object).
|
|
35
|
-
:return: Result of the upload operation.
|
|
36
|
-
"""
|
|
37
|
-
return self._client.upload_asset(app_id, file_name, file)
|
|
38
|
-
|
|
39
|
-
def delete_assets(self, app_id: str) -> ApiResponse | None:
|
|
40
|
-
"""
|
|
41
|
-
Deletes all assets for a specific application from the device.
|
|
42
|
-
|
|
43
|
-
:param app_id: Application ID whose assets should be deleted.
|
|
44
|
-
:return: Result of the delete operation.
|
|
45
|
-
"""
|
|
46
|
-
return self._client.delete_assets(app_id)
|
|
47
|
-
|
|
48
|
-
def draw_display(self, app_id: str, elements: list[dict]) -> ApiResponse | None:
|
|
49
|
-
"""
|
|
50
|
-
Draws elements on the device display.
|
|
51
|
-
|
|
52
|
-
:param app_id: Application ID for organizing display elements.
|
|
53
|
-
:param elements: Array of display elements (text or image).
|
|
54
|
-
:return: Result of the draw operation.
|
|
55
|
-
"""
|
|
56
|
-
return self._client.draw_display(app_id, elements)
|
|
57
|
-
|
|
58
|
-
def clear_display(self) -> ApiResponse | None:
|
|
59
|
-
"""
|
|
60
|
-
Clears the device display.
|
|
61
|
-
|
|
62
|
-
:return: Result of the clear operation.
|
|
63
|
-
"""
|
|
64
|
-
return self._client.clear_display()
|
|
65
|
-
|
|
66
|
-
def play_sound(self, app_id: str, path: str) -> ApiResponse | None:
|
|
67
|
-
"""
|
|
68
|
-
Plays an audio file from the assets directory.
|
|
69
|
-
|
|
70
|
-
:param app_id: Application ID for organizing assets.
|
|
71
|
-
:param path: Path to the audio file within the app's assets directory.
|
|
72
|
-
:return: Result of the play operation.
|
|
73
|
-
"""
|
|
74
|
-
return self._client.play_audio(app_id, path)
|
|
75
|
-
|
|
76
|
-
def stop_sound(self) -> ApiResponse | None:
|
|
77
|
-
"""
|
|
78
|
-
Stops any currently playing audio on the device.
|
|
79
|
-
|
|
80
|
-
:return: Result of the stop operation.
|
|
81
|
-
"""
|
|
82
|
-
return self._client.stop_audio()
|
|
1
|
+
from .client import BusyBar
|
busylib/client.py
CHANGED
|
@@ -1,108 +1,370 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import json
|
|
4
|
+
import typing as tp
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
1
7
|
import requests
|
|
2
|
-
|
|
3
|
-
from busylib
|
|
8
|
+
|
|
9
|
+
from busylib import exceptions, types
|
|
10
|
+
|
|
11
|
+
JsonType = dict[str, tp.Any] | list[tp.Any] | str | int | float | bool | None
|
|
4
12
|
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
def _serialize_for_json(obj):
|
|
15
|
+
if isinstance(obj, enum.Enum):
|
|
16
|
+
return obj.value
|
|
17
|
+
elif isinstance(obj, dict):
|
|
18
|
+
return {k: _serialize_for_json(v) for k, v in obj.items()}
|
|
19
|
+
elif isinstance(obj, list):
|
|
20
|
+
return [_serialize_for_json(item) for item in obj]
|
|
21
|
+
elif dataclasses.is_dataclass(obj):
|
|
22
|
+
return _serialize_for_json(dataclasses.asdict(obj))
|
|
23
|
+
else:
|
|
24
|
+
return obj
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
|
|
27
|
+
class BusyBar:
|
|
28
|
+
"""
|
|
29
|
+
Main library class for interacting with the Busy Bar API.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
14
33
|
self,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
addr: str | None = None,
|
|
35
|
+
*,
|
|
36
|
+
token: str | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
if addr is None and token is None:
|
|
39
|
+
self.base_url = "http://10.0.4.20"
|
|
40
|
+
elif addr is None:
|
|
41
|
+
self.base_url = "https://proxy.dev.busy.app"
|
|
42
|
+
elif addr is not None:
|
|
43
|
+
if "://" not in addr:
|
|
44
|
+
addr = f"http://{addr}"
|
|
45
|
+
self.base_url = addr
|
|
46
|
+
|
|
47
|
+
self.client = requests.Session()
|
|
48
|
+
if token is not None:
|
|
49
|
+
self.client.headers["Authorization"] = f"Bearer {token}"
|
|
50
|
+
|
|
51
|
+
def __enter__(self):
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
55
|
+
self.close()
|
|
56
|
+
|
|
57
|
+
def close(self):
|
|
58
|
+
self.client.close()
|
|
59
|
+
|
|
60
|
+
def _handle_response(
|
|
61
|
+
self, response: requests.Response, as_bytes: bool = False
|
|
62
|
+
) -> bytes | str | JsonType:
|
|
63
|
+
if response.status_code >= 400:
|
|
64
|
+
try:
|
|
65
|
+
error_data = response.json()
|
|
66
|
+
raise exceptions.BusyBarAPIError(
|
|
67
|
+
error=error_data.get("error", "Unknown error"),
|
|
68
|
+
code=error_data.get("code", response.status_code),
|
|
69
|
+
)
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
raise exceptions.BusyBarAPIError(
|
|
72
|
+
error=f"HTTP {response.status_code}: {response.text}",
|
|
73
|
+
code=response.status_code,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if as_bytes:
|
|
77
|
+
return response.content
|
|
78
|
+
|
|
23
79
|
try:
|
|
24
|
-
response
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
self
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
return response.json()
|
|
81
|
+
except json.JSONDecodeError:
|
|
82
|
+
return response.text
|
|
83
|
+
|
|
84
|
+
def get_version(self) -> types.VersionInfo:
|
|
85
|
+
response = self.client.get(urllib.parse.urljoin(self.base_url, "/api/version"))
|
|
86
|
+
data = self._handle_response(response)
|
|
87
|
+
return types.VersionInfo(**data)
|
|
88
|
+
|
|
89
|
+
def update_firmware(
|
|
90
|
+
self, firmware_data: bytes, name: str | None = None
|
|
91
|
+
) -> types.SuccessResponse:
|
|
92
|
+
params = {}
|
|
93
|
+
if name:
|
|
94
|
+
params["name"] = name
|
|
95
|
+
|
|
96
|
+
response = self.client.post(
|
|
97
|
+
urllib.parse.urljoin(self.base_url, "/api/update"),
|
|
98
|
+
params=params,
|
|
99
|
+
data=firmware_data,
|
|
100
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
101
|
+
)
|
|
102
|
+
data = self._handle_response(response)
|
|
103
|
+
return types.SuccessResponse(**data)
|
|
104
|
+
|
|
105
|
+
def get_status(self) -> types.Status:
|
|
106
|
+
response = self.client.get(urllib.parse.urljoin(self.base_url, "/api/status"))
|
|
107
|
+
data = self._handle_response(response)
|
|
108
|
+
|
|
109
|
+
system = None
|
|
110
|
+
if data.get("system"):
|
|
111
|
+
system = types.StatusSystem(**data["system"])
|
|
112
|
+
|
|
113
|
+
power = None
|
|
114
|
+
if data.get("power"):
|
|
115
|
+
power_data = data["power"]
|
|
116
|
+
|
|
117
|
+
if power_data.get("state"):
|
|
118
|
+
power_data["state"] = types.PowerState(power_data["state"])
|
|
119
|
+
|
|
120
|
+
power = types.StatusPower(**power_data)
|
|
121
|
+
|
|
122
|
+
return types.Status(system=system, power=power)
|
|
123
|
+
|
|
124
|
+
def get_system_status(self) -> types.StatusSystem:
|
|
125
|
+
response = self.client.get(
|
|
126
|
+
urllib.parse.urljoin(self.base_url, "/api/status/system")
|
|
127
|
+
)
|
|
128
|
+
data = self._handle_response(response)
|
|
129
|
+
return types.StatusSystem(**data)
|
|
130
|
+
|
|
131
|
+
def get_power_status(self) -> types.StatusPower:
|
|
132
|
+
response = self.client.get(
|
|
133
|
+
urllib.parse.urljoin(self.base_url, "/api/status/power")
|
|
134
|
+
)
|
|
135
|
+
data = self._handle_response(response)
|
|
136
|
+
|
|
137
|
+
if data.get("state"):
|
|
138
|
+
data["state"] = types.PowerState(data["state"])
|
|
139
|
+
|
|
140
|
+
return types.StatusPower(**data)
|
|
141
|
+
|
|
142
|
+
def write_storage_file(self, path: str, data: bytes) -> types.SuccessResponse:
|
|
143
|
+
response = self.client.post(
|
|
144
|
+
urllib.parse.urljoin(self.base_url, "/api/storage/write"),
|
|
145
|
+
params={"path": path},
|
|
146
|
+
data=data,
|
|
147
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
148
|
+
)
|
|
149
|
+
data = self._handle_response(response)
|
|
150
|
+
return types.SuccessResponse(**data)
|
|
151
|
+
|
|
152
|
+
def read_storage_file(self, path: str) -> bytes:
|
|
153
|
+
response = self.client.get(
|
|
154
|
+
urllib.parse.urljoin(self.base_url, "/api/storage/read"),
|
|
155
|
+
params={"path": path},
|
|
156
|
+
)
|
|
157
|
+
return self._handle_response(response, as_bytes=True)
|
|
158
|
+
|
|
159
|
+
def list_storage_files(self, path: str) -> types.StorageList:
|
|
160
|
+
response = self.client.get(
|
|
161
|
+
urllib.parse.urljoin(self.base_url, "/api/storage/list"),
|
|
162
|
+
params={"path": path},
|
|
163
|
+
)
|
|
164
|
+
data = self._handle_response(response)
|
|
165
|
+
|
|
166
|
+
elements = []
|
|
167
|
+
for item in data.get("list", []):
|
|
168
|
+
if item["type"] == "file":
|
|
169
|
+
elements.append(types.StorageFileElement(**item))
|
|
170
|
+
elif item["type"] == "dir":
|
|
171
|
+
elements.append(types.StorageDirElement(**item))
|
|
172
|
+
|
|
173
|
+
return types.StorageList(list=elements)
|
|
174
|
+
|
|
175
|
+
def remove_storage_file(self, path: str) -> types.SuccessResponse:
|
|
176
|
+
response = self.client.delete(
|
|
177
|
+
urllib.parse.urljoin(self.base_url, "/api/storage/remove"),
|
|
178
|
+
params={"path": path},
|
|
179
|
+
)
|
|
180
|
+
data = self._handle_response(response)
|
|
181
|
+
return types.SuccessResponse(**data)
|
|
182
|
+
|
|
183
|
+
def create_storage_directory(self, path: str) -> types.SuccessResponse:
|
|
184
|
+
response = self.client.post(
|
|
185
|
+
urllib.parse.urljoin(self.base_url, "/api/storage/mkdir"),
|
|
186
|
+
params={"path": path},
|
|
187
|
+
)
|
|
188
|
+
data = self._handle_response(response)
|
|
189
|
+
return types.SuccessResponse(**data)
|
|
60
190
|
|
|
61
191
|
def upload_asset(
|
|
62
|
-
self, app_id: str,
|
|
63
|
-
) ->
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
192
|
+
self, app_id: str, filename: str, data: bytes
|
|
193
|
+
) -> types.SuccessResponse:
|
|
194
|
+
response = self.client.post(
|
|
195
|
+
urllib.parse.urljoin(self.base_url, "/api/assets/upload"),
|
|
196
|
+
params={"app_id": app_id, "file": filename},
|
|
197
|
+
data=data,
|
|
198
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
199
|
+
)
|
|
200
|
+
data = self._handle_response(response)
|
|
201
|
+
return types.SuccessResponse(**data)
|
|
202
|
+
|
|
203
|
+
def delete_app_assets(self, app_id: str) -> types.SuccessResponse:
|
|
204
|
+
response = self.client.delete(
|
|
205
|
+
urllib.parse.urljoin(self.base_url, "/api/assets/upload"),
|
|
206
|
+
params={"app_id": app_id},
|
|
207
|
+
)
|
|
208
|
+
data = self._handle_response(response)
|
|
209
|
+
return types.SuccessResponse(**data)
|
|
210
|
+
|
|
211
|
+
def draw_on_display(
|
|
212
|
+
self, display_data: types.DisplayElements
|
|
213
|
+
) -> types.SuccessResponse:
|
|
214
|
+
response = self.client.post(
|
|
215
|
+
urllib.parse.urljoin(self.base_url, "/api/display/draw"),
|
|
216
|
+
json=_serialize_for_json(display_data),
|
|
217
|
+
headers={"Content-Type": "application/json"},
|
|
218
|
+
)
|
|
219
|
+
data = self._handle_response(response)
|
|
220
|
+
return types.SuccessResponse(**data)
|
|
221
|
+
|
|
222
|
+
def clear_display(self) -> types.SuccessResponse:
|
|
223
|
+
response = self.client.delete(
|
|
224
|
+
urllib.parse.urljoin(self.base_url, "/api/display/draw")
|
|
225
|
+
)
|
|
226
|
+
data = self._handle_response(response)
|
|
227
|
+
return types.SuccessResponse(**data)
|
|
228
|
+
|
|
229
|
+
def get_display_brightness(self) -> types.DisplayBrightnessInfo:
|
|
230
|
+
response = self.client.get(
|
|
231
|
+
urllib.parse.urljoin(self.base_url, "/api/display/brightness")
|
|
232
|
+
)
|
|
233
|
+
data = self._handle_response(response)
|
|
234
|
+
return types.DisplayBrightnessInfo(**data)
|
|
235
|
+
|
|
236
|
+
def set_display_brightness(
|
|
237
|
+
self, front: str | None = None, back: str | None = None
|
|
238
|
+
) -> types.SuccessResponse:
|
|
239
|
+
params = {}
|
|
240
|
+
if front is not None:
|
|
241
|
+
params["front"] = front
|
|
242
|
+
if back is not None:
|
|
243
|
+
params["back"] = back
|
|
244
|
+
|
|
245
|
+
response = self.client.post(
|
|
246
|
+
urllib.parse.urljoin(self.base_url, "/api/display/brightness"),
|
|
247
|
+
params=params,
|
|
248
|
+
)
|
|
249
|
+
data = self._handle_response(response)
|
|
250
|
+
return types.SuccessResponse(**data)
|
|
251
|
+
|
|
252
|
+
def play_audio(self, app_id: str, path: str) -> types.SuccessResponse:
|
|
253
|
+
response = self.client.post(
|
|
254
|
+
urllib.parse.urljoin(self.base_url, "/api/audio/play"),
|
|
255
|
+
params={"app_id": app_id, "path": path},
|
|
256
|
+
)
|
|
257
|
+
data = self._handle_response(response)
|
|
258
|
+
return types.SuccessResponse(**data)
|
|
259
|
+
|
|
260
|
+
def stop_audio(self) -> types.SuccessResponse:
|
|
261
|
+
response = self.client.delete(
|
|
262
|
+
urllib.parse.urljoin(self.base_url, "/api/audio/play")
|
|
263
|
+
)
|
|
264
|
+
data = self._handle_response(response)
|
|
265
|
+
return types.SuccessResponse(**data)
|
|
266
|
+
|
|
267
|
+
def get_audio_volume(self) -> types.AudioVolumeInfo:
|
|
268
|
+
response = self.client.get(
|
|
269
|
+
urllib.parse.urljoin(self.base_url, "/api/audio/volume")
|
|
270
|
+
)
|
|
271
|
+
data = self._handle_response(response)
|
|
272
|
+
return types.AudioVolumeInfo(**data)
|
|
273
|
+
|
|
274
|
+
def set_audio_volume(self, volume: float) -> types.SuccessResponse:
|
|
275
|
+
response = self.client.post(
|
|
276
|
+
urllib.parse.urljoin(self.base_url, "/api/audio/volume"),
|
|
277
|
+
params={"volume": volume},
|
|
278
|
+
)
|
|
279
|
+
data = self._handle_response(response)
|
|
280
|
+
return types.SuccessResponse(**data)
|
|
281
|
+
|
|
282
|
+
def send_input_key(self, key: types.InputKey) -> types.SuccessResponse:
|
|
283
|
+
response = self.client.post(
|
|
284
|
+
urllib.parse.urljoin(self.base_url, "/api/input"),
|
|
285
|
+
params={"key": key.value},
|
|
286
|
+
)
|
|
287
|
+
data = self._handle_response(response)
|
|
288
|
+
return types.SuccessResponse(**data)
|
|
289
|
+
|
|
290
|
+
def enable_wifi(self) -> types.SuccessResponse:
|
|
291
|
+
response = self.client.post(
|
|
292
|
+
urllib.parse.urljoin(self.base_url, "/api/wifi/enable")
|
|
293
|
+
)
|
|
294
|
+
data = self._handle_response(response)
|
|
295
|
+
return types.SuccessResponse(**data)
|
|
296
|
+
|
|
297
|
+
def disable_wifi(self) -> types.SuccessResponse:
|
|
298
|
+
response = self.client.post(
|
|
299
|
+
urllib.parse.urljoin(self.base_url, "/api/wifi/disable")
|
|
300
|
+
)
|
|
301
|
+
data = self._handle_response(response)
|
|
302
|
+
return types.SuccessResponse(**data)
|
|
303
|
+
|
|
304
|
+
def get_wifi_status(self) -> types.StatusResponse:
|
|
305
|
+
response = self.client.get(
|
|
306
|
+
urllib.parse.urljoin(self.base_url, "/api/wifi/status")
|
|
307
|
+
)
|
|
308
|
+
data = self._handle_response(response)
|
|
309
|
+
|
|
310
|
+
if data.get("state"):
|
|
311
|
+
data["state"] = types.WifiState(data["state"])
|
|
312
|
+
|
|
313
|
+
if data.get("security"):
|
|
314
|
+
data["security"] = types.WifiSecurityMethod(data["security"])
|
|
315
|
+
|
|
316
|
+
if data.get("ip_config"):
|
|
317
|
+
ip_config_data = data["ip_config"]
|
|
318
|
+
if ip_config_data.get("ip_method"):
|
|
319
|
+
ip_config_data["ip_method"] = types.WifiIpMethod(
|
|
320
|
+
ip_config_data["ip_method"]
|
|
321
|
+
)
|
|
322
|
+
if ip_config_data.get("ip_type"):
|
|
323
|
+
ip_config_data["ip_type"] = types.WifiIpType(ip_config_data["ip_type"])
|
|
324
|
+
data["ip_config"] = types.WifiIpConfig(**ip_config_data)
|
|
325
|
+
|
|
326
|
+
return types.StatusResponse(**data)
|
|
327
|
+
|
|
328
|
+
def connect_wifi(self, config: types.ConnectRequestConfig) -> types.SuccessResponse:
|
|
329
|
+
response = self.client.post(
|
|
330
|
+
urllib.parse.urljoin(self.base_url, "/api/wifi/connect"),
|
|
331
|
+
json=_serialize_for_json(config),
|
|
332
|
+
headers={"Content-Type": "application/json"},
|
|
333
|
+
)
|
|
334
|
+
data = self._handle_response(response)
|
|
335
|
+
return types.SuccessResponse(**data)
|
|
336
|
+
|
|
337
|
+
def disconnect_wifi(self) -> types.SuccessResponse:
|
|
338
|
+
response = self.client.post(
|
|
339
|
+
urllib.parse.urljoin(self.base_url, "/api/wifi/disconnect")
|
|
340
|
+
)
|
|
341
|
+
data = self._handle_response(response)
|
|
342
|
+
return types.SuccessResponse(**data)
|
|
343
|
+
|
|
344
|
+
def scan_wifi_networks(self) -> types.NetworkResponse:
|
|
345
|
+
response = self.client.get(
|
|
346
|
+
urllib.parse.urljoin(self.base_url, "/api/wifi/networks")
|
|
347
|
+
)
|
|
348
|
+
data = self._handle_response(response)
|
|
349
|
+
|
|
350
|
+
networks = []
|
|
351
|
+
if data.get("networks"):
|
|
352
|
+
for network_data in data["networks"]:
|
|
353
|
+
if network_data.get("security"):
|
|
354
|
+
network_data["security"] = types.WifiSecurityMethod(
|
|
355
|
+
network_data["security"]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
networks.append(types.Network(**network_data))
|
|
359
|
+
|
|
360
|
+
return types.NetworkResponse(
|
|
361
|
+
count=data.get("count"),
|
|
362
|
+
networks=networks or None,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def get_screen_frame(self, display: int) -> bytes:
|
|
366
|
+
response = self.client.get(
|
|
367
|
+
urllib.parse.urljoin(self.base_url, "/api/screen"),
|
|
368
|
+
params={"display": display},
|
|
369
|
+
)
|
|
370
|
+
return self._handle_response(response, as_bytes=True)
|
busylib/exceptions.py
ADDED
busylib/types.py
CHANGED
|
@@ -1,9 +1,210 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import typing as tp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WifiSecurityMethod(enum.Enum):
|
|
7
|
+
OPEN = "Open"
|
|
8
|
+
WPA = "WPA"
|
|
9
|
+
WPA2 = "WPA2"
|
|
10
|
+
WEP = "WEP"
|
|
11
|
+
WPA_ENTERPRISE = "WPA (Enterprise)"
|
|
12
|
+
WPA2_ENTERPRISE = "WPA2 (Enterprise)"
|
|
13
|
+
WPA_WPA2 = "WPA/WPA2"
|
|
14
|
+
WPA3 = "WPA3"
|
|
15
|
+
WPA2_WPA3 = "WPA2/WPA3"
|
|
16
|
+
WPA3_ENTERPRISE = "WPA3 (Enterprise)"
|
|
17
|
+
WPA2_WPA3_ENTERPRISE = "WPA2/WPA3 (Enterprise)"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WifiIpMethod(enum.Enum):
|
|
21
|
+
DHCP = "dhcp"
|
|
22
|
+
STATIC = "static"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WifiIpType(enum.Enum):
|
|
26
|
+
IPV4 = "ipv4"
|
|
27
|
+
IPV6 = "ipv6"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PowerState(enum.Enum):
|
|
31
|
+
DISCHARGING = "discharging"
|
|
32
|
+
CHARGING = "charging"
|
|
33
|
+
CHARGED = "charged"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WifiState(enum.Enum):
|
|
37
|
+
DISABLED = "disabled"
|
|
38
|
+
ENABLED = "enabled"
|
|
39
|
+
CONNECTED = "connected"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ElementType(enum.Enum):
|
|
43
|
+
FILE = "file"
|
|
44
|
+
DIR = "dir"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DisplayElementType(enum.Enum):
|
|
48
|
+
TEXT = "text"
|
|
49
|
+
IMAGE = "image"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DisplayName(enum.Enum):
|
|
53
|
+
FRONT = "front"
|
|
54
|
+
BACK = "back"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class InputKey(enum.Enum):
|
|
58
|
+
UP = "up"
|
|
59
|
+
DOWN = "down"
|
|
60
|
+
OK = "ok"
|
|
61
|
+
BACK = "back"
|
|
62
|
+
START = "start"
|
|
63
|
+
BUSY = "busy"
|
|
64
|
+
STATUS = "status"
|
|
65
|
+
OFF = "off"
|
|
66
|
+
APPS = "apps"
|
|
67
|
+
SETTINGS = "settings"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclasses.dataclass(frozen=True)
|
|
71
|
+
class SuccessResponse:
|
|
72
|
+
result: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclasses.dataclass(frozen=True)
|
|
76
|
+
class Error:
|
|
77
|
+
error: str
|
|
78
|
+
code: int | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclasses.dataclass(frozen=True)
|
|
82
|
+
class VersionInfo:
|
|
83
|
+
api_semver: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclasses.dataclass(frozen=True)
|
|
87
|
+
class StatusSystem:
|
|
88
|
+
version: str | None = None
|
|
89
|
+
uptime: str | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclasses.dataclass(frozen=True)
|
|
93
|
+
class StatusPower:
|
|
94
|
+
state: PowerState | None = None
|
|
95
|
+
battery_charge: int | None = None
|
|
96
|
+
battery_voltage: int | None = None
|
|
97
|
+
battery_current: int | None = None
|
|
98
|
+
usb_voltage: int | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclasses.dataclass(frozen=True)
|
|
102
|
+
class Status:
|
|
103
|
+
system: StatusSystem | None = None
|
|
104
|
+
power: StatusPower | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclasses.dataclass(frozen=True)
|
|
108
|
+
class StorageFileElement:
|
|
109
|
+
type: tp.Literal["file"]
|
|
110
|
+
name: str
|
|
111
|
+
size: int
|
|
2
112
|
|
|
3
113
|
|
|
4
114
|
@dataclasses.dataclass(frozen=True)
|
|
5
|
-
class
|
|
6
|
-
|
|
115
|
+
class StorageDirElement:
|
|
116
|
+
type: tp.Literal["dir"]
|
|
117
|
+
name: str
|
|
7
118
|
|
|
8
|
-
|
|
9
|
-
|
|
119
|
+
|
|
120
|
+
StorageListElement = StorageFileElement | StorageDirElement
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclasses.dataclass(frozen=True)
|
|
124
|
+
class StorageList:
|
|
125
|
+
list: list[StorageListElement]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclasses.dataclass(frozen=True)
|
|
129
|
+
class TextElement:
|
|
130
|
+
id: str
|
|
131
|
+
type: tp.Literal["text"]
|
|
132
|
+
x: int
|
|
133
|
+
y: int
|
|
134
|
+
text: str
|
|
135
|
+
timeout: int | None = None
|
|
136
|
+
display: DisplayName | None = DisplayName.FRONT
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclasses.dataclass(frozen=True)
|
|
140
|
+
class ImageElement:
|
|
141
|
+
id: str
|
|
142
|
+
type: tp.Literal["image"]
|
|
143
|
+
x: int
|
|
144
|
+
y: int
|
|
145
|
+
path: str
|
|
146
|
+
timeout: int | None = None
|
|
147
|
+
display: DisplayName | None = DisplayName.FRONT
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
DisplayElement = TextElement | ImageElement
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclasses.dataclass(frozen=True)
|
|
154
|
+
class DisplayElements:
|
|
155
|
+
app_id: str
|
|
156
|
+
elements: list[DisplayElement]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclasses.dataclass(frozen=True)
|
|
160
|
+
class DisplayBrightnessInfo:
|
|
161
|
+
front: str | None = None
|
|
162
|
+
back: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclasses.dataclass(frozen=True)
|
|
166
|
+
class AudioVolumeInfo:
|
|
167
|
+
volume: float | None = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclasses.dataclass(frozen=True)
|
|
171
|
+
class WifiIpConfig:
|
|
172
|
+
ip_method: WifiIpMethod | None = None
|
|
173
|
+
ip_type: WifiIpType | None = None
|
|
174
|
+
address: str | None = None
|
|
175
|
+
mask: str | None = None
|
|
176
|
+
gateway: str | None = None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclasses.dataclass(frozen=True)
|
|
180
|
+
class Network:
|
|
181
|
+
ssid: str | None = None
|
|
182
|
+
security: WifiSecurityMethod | None = None
|
|
183
|
+
rssi: int | None = None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclasses.dataclass(frozen=True)
|
|
187
|
+
class StatusResponse:
|
|
188
|
+
state: WifiState | None = None
|
|
189
|
+
ssid: str | None = None
|
|
190
|
+
security: WifiSecurityMethod | None = None
|
|
191
|
+
ip_config: WifiIpConfig | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclasses.dataclass(frozen=True)
|
|
195
|
+
class ConnectRequestConfig:
|
|
196
|
+
ssid: str | None = None
|
|
197
|
+
password: str | None = None
|
|
198
|
+
security: WifiSecurityMethod | None = None
|
|
199
|
+
ip_config: WifiIpConfig | None = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclasses.dataclass(frozen=True)
|
|
203
|
+
class NetworkResponse:
|
|
204
|
+
count: int | None = None
|
|
205
|
+
networks: list[Network] | None = None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclasses.dataclass(frozen=True)
|
|
209
|
+
class ScreenResponse:
|
|
210
|
+
data: str # base64 encoded image data
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: busylib
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python library for Busy Bar API
|
|
5
|
+
Author-email: flipperdevices <pypi@flipperdevices.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/busy-app/busylib-py
|
|
7
|
+
Project-URL: Repository, https://github.com/busy-app/busylib-py
|
|
8
|
+
Project-URL: Documentation, https://busylib.readthedocs.io
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: requests==2.32.4
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest==8.4.1; extra == "dev"
|
|
22
|
+
Requires-Dist: requests-mock==1.12.1; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# busylib
|
|
27
|
+
|
|
28
|
+
A simple and intuitive Python client for interacting with the Busy Bar API. This library allows you to programmatically control the device's display, audio, and assets.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- Easy-to-use API for all major device functions.
|
|
33
|
+
- Upload and manage assets for your applications.
|
|
34
|
+
- Control the display by drawing text and images.
|
|
35
|
+
- Play and stop audio files.
|
|
36
|
+
- Built-in validation for device IP addresses.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
You can install `busylib` directly from PyPI:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install busylib
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
First, import and initialize the `BusyBar` client with IP address of your device.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from busylib import BusyBar
|
|
52
|
+
|
|
53
|
+
bb = BusyBar("10.0.4.20")
|
|
54
|
+
|
|
55
|
+
version_info = bb.get_version()
|
|
56
|
+
print(f"Device version: {version_info.version}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
You can also use context manager.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from busylib import BusyBar
|
|
63
|
+
|
|
64
|
+
with BusyBar("10.0.4.20") as bb:
|
|
65
|
+
version_info = bb.get_version()
|
|
66
|
+
print(f"Device version: {version_info.version}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API Examples
|
|
70
|
+
|
|
71
|
+
Here are some examples of how to use the library to control your Busy Bar device.
|
|
72
|
+
|
|
73
|
+
### Uploading an Asset
|
|
74
|
+
|
|
75
|
+
You can upload files (like images or sounds) to be used by your application on the device.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
with open("path/to/your/image.png", "rb") as f:
|
|
79
|
+
file_bytes = f.read()
|
|
80
|
+
response = bb.upload_asset(
|
|
81
|
+
app_id="my-app",
|
|
82
|
+
filename="logo.png",
|
|
83
|
+
data=file_bytes
|
|
84
|
+
)
|
|
85
|
+
print(f"Upload result: {response.result}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
with open("path/to/your/sound.wav", "rb") as f:
|
|
89
|
+
file_bytes = f.read()
|
|
90
|
+
response = bb.upload_asset(
|
|
91
|
+
app_id="my-app",
|
|
92
|
+
filename="notification.wav",
|
|
93
|
+
data=file_bytes
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Drawing on the Display
|
|
98
|
+
|
|
99
|
+
Draw text or images on the device's screen. The `draw_on_display` method accepts a `DisplayElements` object containing a list of elements to render.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from busylib import types
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
text_element = types.TextElement(
|
|
106
|
+
id="hello",
|
|
107
|
+
type="text",
|
|
108
|
+
x=10,
|
|
109
|
+
y=20,
|
|
110
|
+
text="Hello, World!",
|
|
111
|
+
display=types.DisplayName.FRONT,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
image_element = types.ImageElement(
|
|
115
|
+
id="logo",
|
|
116
|
+
type="image",
|
|
117
|
+
x=50,
|
|
118
|
+
y=40,
|
|
119
|
+
path="logo.png",
|
|
120
|
+
display=types.DisplayName.BACK,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
display_data = types.DisplayElements(
|
|
124
|
+
app_id="my-app",
|
|
125
|
+
elements=[text_element, image_element]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
response = bb.draw_on_display(display_data)
|
|
129
|
+
print(f"Draw result: {response.result}")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Clearing the Display
|
|
133
|
+
|
|
134
|
+
To clear everything from the screen:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
response = bb.clear_display()
|
|
138
|
+
print(f"Clear result: {response.result}")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Playing Audio
|
|
142
|
+
|
|
143
|
+
Play an audio file that you have already uploaded.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
response = bb.play_audio(app_id="my-app", path="notification.wav")
|
|
147
|
+
print(f"Play result: {response.result}")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Stopping Audio
|
|
151
|
+
|
|
152
|
+
To stop any audio that is currently playing:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
response = bb.stop_audio()
|
|
156
|
+
print(f"Stop result: {response.result}")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Deleting All Assets for an App
|
|
160
|
+
|
|
161
|
+
This will remove all files associated with a specific `app_id`.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
response = bb.delete_app_assets(app_id="my-app")
|
|
165
|
+
print(f"Delete result: {response.result}")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Getting Device Status
|
|
169
|
+
|
|
170
|
+
You can get various status information from the device:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
version = bb.get_version()
|
|
174
|
+
print(f"Version: {version.version}, Branch: {version.branch}")
|
|
175
|
+
|
|
176
|
+
status = bb.get_status()
|
|
177
|
+
if status.system:
|
|
178
|
+
print(f"Uptime: {status.system.uptime}")
|
|
179
|
+
if status.power:
|
|
180
|
+
print(f"Battery: {status.power.battery_charge}%")
|
|
181
|
+
|
|
182
|
+
brightness = bb.get_display_brightness()
|
|
183
|
+
print(f"Front brightness: {brightness.front}, Back brightness: {brightness.back}")
|
|
184
|
+
|
|
185
|
+
volume = bb.get_audio_volume()
|
|
186
|
+
print(f"Volume: {volume.volume}")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Working with Storage
|
|
190
|
+
|
|
191
|
+
You can manage files in the device's storage:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
file_data = b"Hello, world!"
|
|
195
|
+
response = bb.write_storage_file(path="/my-app/data.txt", data=file_data)
|
|
196
|
+
|
|
197
|
+
file_content = bb.read_storage_file(path="/my-app/data.txt")
|
|
198
|
+
print(file_content.decode('utf-8'))
|
|
199
|
+
|
|
200
|
+
storage_list = bb.list_storage_files(path="/my-app")
|
|
201
|
+
for item in storage_list.list:
|
|
202
|
+
if item.type == "file":
|
|
203
|
+
print(f"File: {item.name} ({item.size} bytes)")
|
|
204
|
+
else:
|
|
205
|
+
print(f"Directory: {item.name}")
|
|
206
|
+
|
|
207
|
+
response = bb.create_storage_directory(path="/my-app/subdirectory")
|
|
208
|
+
|
|
209
|
+
response = bb.remove_storage_file(path="/my-app/data.txt")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
To set up a development environment, clone the repository and install the package in editable mode with test dependencies:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
git clone https://github.com/busy-app/busylib
|
|
218
|
+
cd busylib
|
|
219
|
+
python3 -m venv .venv
|
|
220
|
+
source .venv/bin/activate
|
|
221
|
+
make install-dev
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
To run the tests:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
make test
|
|
228
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
busylib/__init__.py,sha256=7CQFe1iS-QRVAVlufLarG0MXEjkbqZht_DI2kUvsHz0,28
|
|
2
|
+
busylib/client.py,sha256=3mm5Xqr0N_KvbdxHdOkV4PWTqcxMHq4hDZziz0ykYKU,13079
|
|
3
|
+
busylib/exceptions.py,sha256=DHYoEXGdADXSxcgLq7BWqq_865J0LsMysZL-Wuip2wc,210
|
|
4
|
+
busylib/types.py,sha256=P6WC7qzV2wJH6VHSKozdv6ujCTA-qrl5i25LVIewkic,4169
|
|
5
|
+
busylib-0.2.0.dist-info/licenses/LICENSE,sha256=aJO9BQGuVb1fvGzHcgWYiwcC9kEFyD-FBb8SPO-ATJ4,1071
|
|
6
|
+
busylib-0.2.0.dist-info/METADATA,sha256=VKuyfOWBb_C-mgQoC2jMXLX0X1Sw7oDjjXX6TiEXtRw,5518
|
|
7
|
+
busylib-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
busylib-0.2.0.dist-info/top_level.txt,sha256=tXeqg2EVVj4E8-ywimkxRzuCJq2HqMXA0571IB15PDA,8
|
|
9
|
+
busylib-0.2.0.dist-info/RECORD,,
|
busylib-0.0.2.dist-info/METADATA
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: busylib
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: Python library for Busy Bar API
|
|
5
|
-
Author-email: Valerii Lisai <v.lisai@flipperdevices.com>
|
|
6
|
-
Project-URL: Homepage, https://github.com/busy-app/busylib
|
|
7
|
-
Project-URL: Repository, https://github.com/busy-app/busylib
|
|
8
|
-
Project-URL: Documentation, https://busylib.readthedocs.io
|
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
Requires-Python: >=3.10
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
License-File: LICENSE
|
|
20
|
-
Requires-Dist: requests==2.32.4
|
|
21
|
-
Provides-Extra: dev
|
|
22
|
-
Requires-Dist: pytest==8.4.1; extra == "dev"
|
|
23
|
-
Requires-Dist: requests-mock==1.12.1; extra == "dev"
|
|
24
|
-
Requires-Dist: ruff; extra == "dev"
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
|
|
27
|
-
# busylib
|
|
28
|
-
|
|
29
|
-
A simple and intuitive Python client for interacting with the Busy Bar API. This library allows you to programmatically control the device's display, audio, and assets.
|
|
30
|
-
|
|
31
|
-
## Features
|
|
32
|
-
|
|
33
|
-
- Easy-to-use API for all major device functions.
|
|
34
|
-
- Upload and manage assets for your applications.
|
|
35
|
-
- Control the display by drawing text and images.
|
|
36
|
-
- Play and stop audio files.
|
|
37
|
-
- Built-in validation for device IP addresses.
|
|
38
|
-
|
|
39
|
-
## Installation
|
|
40
|
-
|
|
41
|
-
You can install `busylib` directly from PyPI:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
pip install busylib
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Usage
|
|
48
|
-
|
|
49
|
-
First, import and initialize the `BusyBar` client with the IP address of your device.
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
from busylib import BusyBar
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
# Default IP is 10.0.4.20, but you can specify your own
|
|
56
|
-
bb = BusyBar("192.168.1.100")
|
|
57
|
-
except ValueError as e:
|
|
58
|
-
print(f"Error: {e}")
|
|
59
|
-
|
|
60
|
-
# Now you can use the bb object to interact with your device.
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## API Examples
|
|
64
|
-
|
|
65
|
-
Here are some examples of how to use the library to control your Busy Bar device.
|
|
66
|
-
|
|
67
|
-
### Uploading an Asset
|
|
68
|
-
|
|
69
|
-
You can upload files (like images or sounds) to be used by your application on the device.
|
|
70
|
-
|
|
71
|
-
```python
|
|
72
|
-
# Upload a file from bytes
|
|
73
|
-
with open("path/to/your/image.png", "rb") as f:
|
|
74
|
-
file_bytes = f.read()
|
|
75
|
-
bb.upload_asset(
|
|
76
|
-
app_id="my-app",
|
|
77
|
-
file_name="logo.png",
|
|
78
|
-
file=file_bytes
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
# Or upload directly from a file-like object
|
|
82
|
-
with open("path/to/your/sound.mp3", "rb") as f:
|
|
83
|
-
bb.upload_asset(
|
|
84
|
-
app_id="my-app",
|
|
85
|
-
file_name="notification.mp3",
|
|
86
|
-
file=f
|
|
87
|
-
)
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Drawing on the Display
|
|
91
|
-
|
|
92
|
-
Draw text or images on the device's screen. The `draw_display` method accepts a list of elements to render.
|
|
93
|
-
|
|
94
|
-
```python
|
|
95
|
-
elements = [
|
|
96
|
-
{
|
|
97
|
-
"type": "text",
|
|
98
|
-
"value": "Hello, World!",
|
|
99
|
-
"x": 10,
|
|
100
|
-
"y": 20,
|
|
101
|
-
"color": "#FFFFFF" # Optional
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
"type": "image",
|
|
105
|
-
"path": "logo.png", # Must be uploaded first
|
|
106
|
-
"x": 50,
|
|
107
|
-
"y": 40
|
|
108
|
-
}
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
bb.draw_display(app_id="my-app", elements=elements)
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Clearing the Display
|
|
115
|
-
|
|
116
|
-
To clear everything from the screen:
|
|
117
|
-
|
|
118
|
-
```python
|
|
119
|
-
bb.clear_display()
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Playing a Sound
|
|
123
|
-
|
|
124
|
-
Play an audio file that you have already uploaded.
|
|
125
|
-
|
|
126
|
-
```python
|
|
127
|
-
bb.play_sound(app_id="my-app", path="notification.mp3")
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Stopping a Sound
|
|
131
|
-
|
|
132
|
-
To stop any audio that is currently playing:
|
|
133
|
-
|
|
134
|
-
```python
|
|
135
|
-
bb.stop_sound()
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Deleting All Assets for an App
|
|
139
|
-
|
|
140
|
-
This will remove all files associated with a specific `app_id`.
|
|
141
|
-
|
|
142
|
-
```python
|
|
143
|
-
bb.delete_assets(app_id="my-app")
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Development
|
|
147
|
-
|
|
148
|
-
To set up a development environment, clone the repository and install the package in editable mode with test dependencies:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
git clone https://github.com/busy-app/busylib
|
|
152
|
-
cd busylib
|
|
153
|
-
python3 -m venv .venv
|
|
154
|
-
source .venv/bin/activate
|
|
155
|
-
make install-dev
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
To run the tests:
|
|
159
|
-
|
|
160
|
-
```bash
|
|
161
|
-
make test
|
|
162
|
-
```
|
busylib-0.0.2.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
busylib/__init__.py,sha256=uxjPUviskpLD81tC5UqRFTTNr1jh7KkvPaKOnUivhFg,2605
|
|
2
|
-
busylib/client.py,sha256=725I_M8AdLUhDI3OjxSZe-giNQsA68lc5Sm1tBFIEJY,3728
|
|
3
|
-
busylib/types.py,sha256=LlBb1-lQraCjAitU-ImI52EXsayXCMcLvEjyuLXsABQ,176
|
|
4
|
-
busylib-0.0.2.dist-info/licenses/LICENSE,sha256=aJO9BQGuVb1fvGzHcgWYiwcC9kEFyD-FBb8SPO-ATJ4,1071
|
|
5
|
-
busylib-0.0.2.dist-info/METADATA,sha256=43fmietVoW8ILVwAV4Wmse8BOH3AoYl49QqccrtH8AU,3824
|
|
6
|
-
busylib-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
busylib-0.0.2.dist-info/top_level.txt,sha256=tXeqg2EVVj4E8-ywimkxRzuCJq2HqMXA0571IB15PDA,8
|
|
8
|
-
busylib-0.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|