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 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
- from typing import IO
3
- from busylib.types import ApiResponse
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
- class ApiClient:
7
- def __init__(self, base_url: str):
8
- if not base_url.endswith("/"):
9
- base_url += "/"
10
- self.base_url = base_url
11
- self.session = requests.Session()
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
- def _request(
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
- method: str,
16
- endpoint: str,
17
- params: dict[str, object] | None = None,
18
- json: object | None = None,
19
- data: object | None = None,
20
- headers: dict[str, str] | None = None,
21
- ) -> ApiResponse:
22
- url = f"{self.base_url}{endpoint}"
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 = self.session.request(
25
- method, url, params=params, json=json, data=data, headers=headers
26
- )
27
- response.raise_for_status()
28
- if response.status_code == 204 or not response.content:
29
- return None
30
- # We expect a JSON response that can be mapped to our dataclass
31
- data = response.json()
32
- return ApiResponse(
33
- success=data.get("success", True), message=data.get("message")
34
- )
35
- except requests.exceptions.RequestException as e:
36
- # Re-raise as a custom exception for better error handling upstream
37
- raise ConnectionError(f"API request failed: {e}") from e
38
-
39
- def post(
40
- self,
41
- endpoint: str,
42
- params: dict[str, object] = None,
43
- json: object | None = None,
44
- data: object | None = None,
45
- headers: dict[str, str] | None = None,
46
- ) -> ApiResponse | None:
47
- return self._request(
48
- "POST", endpoint, params=params, json=json, data=data, headers=headers
49
- )
50
-
51
- def get(
52
- self, endpoint: str, params: dict[str, object] | None = None
53
- ) -> ApiResponse | None:
54
- return self._request("GET", endpoint, params=params)
55
-
56
- def delete(
57
- self, endpoint: str, params: dict[str, object] | None = None
58
- ) -> ApiResponse | None:
59
- return self._request("DELETE", endpoint, params=params)
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, file_name: str, file: bytes | IO[bytes]
63
- ) -> ApiResponse | None:
64
- """
65
- Uploads an asset to the device.
66
- """
67
- payload = {"app_id": app_id, "file": file_name}
68
- headers = {"Content-Type": "application/octet-stream"}
69
- return self.post("v0/assets/upload", params=payload, data=file, headers=headers)
70
-
71
- def delete_assets(self, app_id: str) -> ApiResponse | None:
72
- """
73
- Deletes all assets for a specific application from the device.
74
- """
75
- params = {"app_id": app_id}
76
- return self.delete("v0/assets/upload", params=params)
77
-
78
- def play_audio(self, app_id: str, path: str) -> ApiResponse | None:
79
- """
80
- Plays an audio file from the assets directory.
81
- """
82
- params = {"app_id": app_id, "path": path}
83
- return self.post("v0/audio/play", params=params)
84
-
85
- def stop_audio(self) -> ApiResponse | None:
86
- """
87
- Stops any currently playing audio on the device.
88
- """
89
- return self.delete("v0/audio/play")
90
-
91
- def draw_display(
92
- self, app_id: str, elements: list[dict[str, object]]
93
- ) -> ApiResponse | None:
94
- """
95
- Draws elements on the device display.
96
- """
97
- default_values = {"timeout": 5, "x": 0, "y": 0, "display": "front"}
98
-
99
- normalized_elements = [default_values | e for e in elements]
100
- return self.post(
101
- "v0/display/draw", json={"app_id": app_id, "elements": normalized_elements}
102
- )
103
-
104
- def clear_display(self) -> ApiResponse | None:
105
- """
106
- Clears the device display.
107
- """
108
- return self.delete("v0/display/draw")
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
@@ -0,0 +1,5 @@
1
+ class BusyBarAPIError(Exception):
2
+ def __init__(self, error: str, code: int | None = None):
3
+ self.error = error
4
+ self.code = code
5
+ super().__init__(f"API Error: {error} (code: {code})")
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 ApiResponse:
6
- """A generic response from the BusyBar API"""
115
+ class StorageDirElement:
116
+ type: tp.Literal["dir"]
117
+ name: str
7
118
 
8
- success: bool
9
- message: str | None = None
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,,
@@ -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
- ```
@@ -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,,