busylib 0.0.2__py3-none-any.whl → 0.1.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.
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,359 @@
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
4
-
5
-
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()
12
-
13
- def _request(
14
- 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}"
8
+
9
+ from busylib import exceptions, types
10
+
11
+ JsonType = dict[str, tp.Any] | list[tp.Any] | str | int | float | bool | None
12
+
13
+
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
25
+
26
+
27
+ class BusyBar:
28
+ """
29
+ Main library class for interacting with the Busy Bar API.
30
+ """
31
+
32
+ def __init__(self, addr: str):
33
+ self.base_url = f"http://{addr}"
34
+ self.client = requests.Session()
35
+
36
+ def __enter__(self):
37
+ return self
38
+
39
+ def __exit__(self, exc_type, exc_val, exc_tb):
40
+ self.close()
41
+
42
+ def close(self):
43
+ self.client.close()
44
+
45
+ def _handle_response(
46
+ self, response: requests.Response, as_bytes: bool = False
47
+ ) -> bytes | str | JsonType:
48
+ if response.status_code >= 400:
49
+ try:
50
+ error_data = response.json()
51
+ raise exceptions.BusyBarAPIError(
52
+ error=error_data.get("error", "Unknown error"),
53
+ code=error_data.get("code", response.status_code),
54
+ )
55
+ except json.JSONDecodeError:
56
+ raise exceptions.BusyBarAPIError(
57
+ error=f"HTTP {response.status_code}: {response.text}",
58
+ code=response.status_code,
59
+ )
60
+
61
+ if as_bytes:
62
+ return response.content
63
+
23
64
  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)
65
+ return response.json()
66
+ except json.JSONDecodeError:
67
+ return response.text
68
+
69
+ def get_version(self) -> types.VersionInfo:
70
+ response = self.client.get(
71
+ urllib.parse.urljoin(self.base_url, "/api/v0/version")
72
+ )
73
+ data = self._handle_response(response)
74
+ return types.VersionInfo(**data)
75
+
76
+ def update_firmware(
77
+ self, firmware_data: bytes, name: str | None = None
78
+ ) -> types.SuccessResponse:
79
+ params = {}
80
+ if name:
81
+ params["name"] = name
82
+
83
+ response = self.client.post(
84
+ urllib.parse.urljoin(self.base_url, "/api/v0/update"),
85
+ params=params,
86
+ data=firmware_data,
87
+ headers={"Content-Type": "application/octet-stream"},
88
+ )
89
+ data = self._handle_response(response)
90
+ return types.SuccessResponse(**data)
91
+
92
+ def get_status(self) -> types.Status:
93
+ response = self.client.get(
94
+ urllib.parse.urljoin(self.base_url, "/api/v0/status")
95
+ )
96
+ data = self._handle_response(response)
97
+
98
+ system = None
99
+ if data.get("system"):
100
+ system = types.StatusSystem(**data["system"])
101
+
102
+ power = None
103
+ if data.get("power"):
104
+ power_data = data["power"]
105
+
106
+ if power_data.get("state"):
107
+ power_data["state"] = types.PowerState(power_data["state"])
108
+
109
+ power = types.StatusPower(**power_data)
110
+
111
+ return types.Status(system=system, power=power)
112
+
113
+ def get_system_status(self) -> types.StatusSystem:
114
+ response = self.client.get(
115
+ urllib.parse.urljoin(self.base_url, "/api/v0/status/system")
116
+ )
117
+ data = self._handle_response(response)
118
+ return types.StatusSystem(**data)
119
+
120
+ def get_power_status(self) -> types.StatusPower:
121
+ response = self.client.get(
122
+ urllib.parse.urljoin(self.base_url, "/api/v0/status/power")
123
+ )
124
+ data = self._handle_response(response)
125
+
126
+ if data.get("state"):
127
+ data["state"] = types.PowerState(data["state"])
128
+
129
+ return types.StatusPower(**data)
130
+
131
+ def write_storage_file(self, path: str, data: bytes) -> types.SuccessResponse:
132
+ response = self.client.post(
133
+ urllib.parse.urljoin(self.base_url, "/api/v0/storage/write"),
134
+ params={"path": path},
135
+ data=data,
136
+ headers={"Content-Type": "application/octet-stream"},
137
+ )
138
+ data = self._handle_response(response)
139
+ return types.SuccessResponse(**data)
140
+
141
+ def read_storage_file(self, path: str) -> bytes:
142
+ response = self.client.get(
143
+ urllib.parse.urljoin(self.base_url, "/api/v0/storage/read"),
144
+ params={"path": path},
145
+ )
146
+ return self._handle_response(response, as_bytes=True)
147
+
148
+ def list_storage_files(self, path: str) -> types.StorageList:
149
+ response = self.client.get(
150
+ urllib.parse.urljoin(self.base_url, "/api/v0/storage/list"),
151
+ params={"path": path},
152
+ )
153
+ data = self._handle_response(response)
154
+
155
+ elements = []
156
+ for item in data.get("list", []):
157
+ if item["type"] == "file":
158
+ elements.append(types.StorageFileElement(**item))
159
+ elif item["type"] == "dir":
160
+ elements.append(types.StorageDirElement(**item))
161
+
162
+ return types.StorageList(list=elements)
163
+
164
+ def remove_storage_file(self, path: str) -> types.SuccessResponse:
165
+ response = self.client.delete(
166
+ urllib.parse.urljoin(self.base_url, "/api/v0/storage/remove"),
167
+ params={"path": path},
168
+ )
169
+ data = self._handle_response(response)
170
+ return types.SuccessResponse(**data)
171
+
172
+ def create_storage_directory(self, path: str) -> types.SuccessResponse:
173
+ response = self.client.post(
174
+ urllib.parse.urljoin(self.base_url, "/api/v0/storage/mkdir"),
175
+ params={"path": path},
176
+ )
177
+ data = self._handle_response(response)
178
+ return types.SuccessResponse(**data)
60
179
 
61
180
  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")
181
+ self, app_id: str, filename: str, data: bytes
182
+ ) -> types.SuccessResponse:
183
+ response = self.client.post(
184
+ urllib.parse.urljoin(self.base_url, "/api/v0/assets/upload"),
185
+ params={"app_id": app_id, "file": filename},
186
+ data=data,
187
+ headers={"Content-Type": "application/octet-stream"},
188
+ )
189
+ data = self._handle_response(response)
190
+ return types.SuccessResponse(**data)
191
+
192
+ def delete_app_assets(self, app_id: str) -> types.SuccessResponse:
193
+ response = self.client.delete(
194
+ urllib.parse.urljoin(self.base_url, "/api/v0/assets/upload"),
195
+ params={"app_id": app_id},
196
+ )
197
+ data = self._handle_response(response)
198
+ return types.SuccessResponse(**data)
199
+
200
+ def draw_on_display(
201
+ self, display_data: types.DisplayElements
202
+ ) -> types.SuccessResponse:
203
+ response = self.client.post(
204
+ urllib.parse.urljoin(self.base_url, "/api/v0/display/draw"),
205
+ json=_serialize_for_json(display_data),
206
+ headers={"Content-Type": "application/json"},
207
+ )
208
+ data = self._handle_response(response)
209
+ return types.SuccessResponse(**data)
210
+
211
+ def clear_display(self) -> types.SuccessResponse:
212
+ response = self.client.delete(
213
+ urllib.parse.urljoin(self.base_url, "/api/v0/display/draw")
214
+ )
215
+ data = self._handle_response(response)
216
+ return types.SuccessResponse(**data)
217
+
218
+ def get_display_brightness(self) -> types.DisplayBrightnessInfo:
219
+ response = self.client.get(
220
+ urllib.parse.urljoin(self.base_url, "/api/v0/display/brightness")
221
+ )
222
+ data = self._handle_response(response)
223
+ return types.DisplayBrightnessInfo(**data)
224
+
225
+ def set_display_brightness(
226
+ self, front: str | None = None, back: str | None = None
227
+ ) -> types.SuccessResponse:
228
+ params = {}
229
+ if front is not None:
230
+ params["front"] = front
231
+ if back is not None:
232
+ params["back"] = back
233
+
234
+ response = self.client.post(
235
+ urllib.parse.urljoin(self.base_url, "/api/v0/display/brightness"),
236
+ params=params,
237
+ )
238
+ data = self._handle_response(response)
239
+ return types.SuccessResponse(**data)
240
+
241
+ def play_audio(self, app_id: str, path: str) -> types.SuccessResponse:
242
+ response = self.client.post(
243
+ urllib.parse.urljoin(self.base_url, "/api/v0/audio/play"),
244
+ params={"app_id": app_id, "path": path},
245
+ )
246
+ data = self._handle_response(response)
247
+ return types.SuccessResponse(**data)
248
+
249
+ def stop_audio(self) -> types.SuccessResponse:
250
+ response = self.client.delete(
251
+ urllib.parse.urljoin(self.base_url, "/api/v0/audio/play")
252
+ )
253
+ data = self._handle_response(response)
254
+ return types.SuccessResponse(**data)
255
+
256
+ def get_audio_volume(self) -> types.AudioVolumeInfo:
257
+ response = self.client.get(
258
+ urllib.parse.urljoin(self.base_url, "/api/v0/audio/volume")
259
+ )
260
+ data = self._handle_response(response)
261
+ return types.AudioVolumeInfo(**data)
262
+
263
+ def set_audio_volume(self, volume: float) -> types.SuccessResponse:
264
+ response = self.client.post(
265
+ urllib.parse.urljoin(self.base_url, "/api/v0/audio/volume"),
266
+ params={"volume": volume},
267
+ )
268
+ data = self._handle_response(response)
269
+ return types.SuccessResponse(**data)
270
+
271
+ def send_input_key(self, key: types.InputKey) -> types.SuccessResponse:
272
+ response = self.client.post(
273
+ urllib.parse.urljoin(self.base_url, "/api/v0/input"),
274
+ params={"key": key.value},
275
+ )
276
+ data = self._handle_response(response)
277
+ return types.SuccessResponse(**data)
278
+
279
+ def enable_wifi(self) -> types.SuccessResponse:
280
+ response = self.client.post(
281
+ urllib.parse.urljoin(self.base_url, "/api/v0/wifi/enable")
282
+ )
283
+ data = self._handle_response(response)
284
+ return types.SuccessResponse(**data)
285
+
286
+ def disable_wifi(self) -> types.SuccessResponse:
287
+ response = self.client.post(
288
+ urllib.parse.urljoin(self.base_url, "/api/v0/wifi/disable")
289
+ )
290
+ data = self._handle_response(response)
291
+ return types.SuccessResponse(**data)
292
+
293
+ def get_wifi_status(self) -> types.StatusResponse:
294
+ response = self.client.get(
295
+ urllib.parse.urljoin(self.base_url, "/api/v0/wifi/status")
296
+ )
297
+ data = self._handle_response(response)
298
+
299
+ if data.get("state"):
300
+ data["state"] = types.WifiState(data["state"])
301
+
302
+ if data.get("security"):
303
+ data["security"] = types.WifiSecurityMethod(data["security"])
304
+
305
+ if data.get("ip_config"):
306
+ ip_config_data = data["ip_config"]
307
+ if ip_config_data.get("ip_method"):
308
+ ip_config_data["ip_method"] = types.WifiIpMethod(
309
+ ip_config_data["ip_method"]
310
+ )
311
+ if ip_config_data.get("ip_type"):
312
+ ip_config_data["ip_type"] = types.WifiIpType(ip_config_data["ip_type"])
313
+ data["ip_config"] = types.WifiIpConfig(**ip_config_data)
314
+
315
+ return types.StatusResponse(**data)
316
+
317
+ def connect_wifi(self, config: types.ConnectRequestConfig) -> types.SuccessResponse:
318
+ response = self.client.post(
319
+ urllib.parse.urljoin(self.base_url, "/api/v0/wifi/connect"),
320
+ json=_serialize_for_json(config),
321
+ headers={"Content-Type": "application/json"},
322
+ )
323
+ data = self._handle_response(response)
324
+ return types.SuccessResponse(**data)
325
+
326
+ def disconnect_wifi(self) -> types.SuccessResponse:
327
+ response = self.client.post(
328
+ urllib.parse.urljoin(self.base_url, "/api/v0/wifi/disconnect")
329
+ )
330
+ data = self._handle_response(response)
331
+ return types.SuccessResponse(**data)
332
+
333
+ def scan_wifi_networks(self) -> types.NetworkResponse:
334
+ response = self.client.get(
335
+ urllib.parse.urljoin(self.base_url, "/api/v0/wifi/networks")
336
+ )
337
+ data = self._handle_response(response)
338
+
339
+ networks = []
340
+ if data.get("networks"):
341
+ for network_data in data["networks"]:
342
+ if network_data.get("security"):
343
+ network_data["security"] = types.WifiSecurityMethod(
344
+ network_data["security"]
345
+ )
346
+
347
+ networks.append(types.Network(**network_data))
348
+
349
+ return types.NetworkResponse(
350
+ count=data.get("count"),
351
+ networks=networks or None,
352
+ )
353
+
354
+ def get_screen_frame(self, display: int) -> bytes:
355
+ response = self.client.get(
356
+ urllib.parse.urljoin(self.base_url, "/api/v0/screen"),
357
+ params={"display": display},
358
+ )
359
+ 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,213 @@
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
+ branch: str
84
+ version: str
85
+ build_date: str
86
+ commit_hash: str
87
+
88
+
89
+ @dataclasses.dataclass(frozen=True)
90
+ class StatusSystem:
91
+ version: str | None = None
92
+ uptime: str | None = None
93
+
94
+
95
+ @dataclasses.dataclass(frozen=True)
96
+ class StatusPower:
97
+ state: PowerState | None = None
98
+ battery_charge: int | None = None
99
+ battery_voltage: int | None = None
100
+ battery_current: int | None = None
101
+ usb_voltage: int | None = None
102
+
103
+
104
+ @dataclasses.dataclass(frozen=True)
105
+ class Status:
106
+ system: StatusSystem | None = None
107
+ power: StatusPower | None = None
108
+
109
+
110
+ @dataclasses.dataclass(frozen=True)
111
+ class StorageFileElement:
112
+ type: tp.Literal["file"]
113
+ name: str
114
+ size: int
2
115
 
3
116
 
4
117
  @dataclasses.dataclass(frozen=True)
5
- class ApiResponse:
6
- """A generic response from the BusyBar API"""
118
+ class StorageDirElement:
119
+ type: tp.Literal["dir"]
120
+ name: str
7
121
 
8
- success: bool
9
- message: str | None = None
122
+
123
+ StorageListElement = StorageFileElement | StorageDirElement
124
+
125
+
126
+ @dataclasses.dataclass(frozen=True)
127
+ class StorageList:
128
+ list: list[StorageListElement]
129
+
130
+
131
+ @dataclasses.dataclass(frozen=True)
132
+ class TextElement:
133
+ id: str
134
+ type: tp.Literal["text"]
135
+ x: int
136
+ y: int
137
+ text: str
138
+ timeout: int | None = None
139
+ display: DisplayName | None = DisplayName.FRONT
140
+
141
+
142
+ @dataclasses.dataclass(frozen=True)
143
+ class ImageElement:
144
+ id: str
145
+ type: tp.Literal["image"]
146
+ x: int
147
+ y: int
148
+ path: str
149
+ timeout: int | None = None
150
+ display: DisplayName | None = DisplayName.FRONT
151
+
152
+
153
+ DisplayElement = TextElement | ImageElement
154
+
155
+
156
+ @dataclasses.dataclass(frozen=True)
157
+ class DisplayElements:
158
+ app_id: str
159
+ elements: list[DisplayElement]
160
+
161
+
162
+ @dataclasses.dataclass(frozen=True)
163
+ class DisplayBrightnessInfo:
164
+ front: str | None = None
165
+ back: str | None = None
166
+
167
+
168
+ @dataclasses.dataclass(frozen=True)
169
+ class AudioVolumeInfo:
170
+ volume: float | None = None
171
+
172
+
173
+ @dataclasses.dataclass(frozen=True)
174
+ class WifiIpConfig:
175
+ ip_method: WifiIpMethod | None = None
176
+ ip_type: WifiIpType | None = None
177
+ address: str | None = None
178
+ mask: str | None = None
179
+ gateway: str | None = None
180
+
181
+
182
+ @dataclasses.dataclass(frozen=True)
183
+ class Network:
184
+ ssid: str | None = None
185
+ security: WifiSecurityMethod | None = None
186
+ rssi: int | None = None
187
+
188
+
189
+ @dataclasses.dataclass(frozen=True)
190
+ class StatusResponse:
191
+ state: WifiState | None = None
192
+ ssid: str | None = None
193
+ security: WifiSecurityMethod | None = None
194
+ ip_config: WifiIpConfig | None = None
195
+
196
+
197
+ @dataclasses.dataclass(frozen=True)
198
+ class ConnectRequestConfig:
199
+ ssid: str | None = None
200
+ password: str | None = None
201
+ security: WifiSecurityMethod | None = None
202
+ ip_config: WifiIpConfig | None = None
203
+
204
+
205
+ @dataclasses.dataclass(frozen=True)
206
+ class NetworkResponse:
207
+ count: int | None = None
208
+ networks: list[Network] | None = None
209
+
210
+
211
+ @dataclasses.dataclass(frozen=True)
212
+ class ScreenResponse:
213
+ data: str # base64 encoded image data
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: busylib
3
+ Version: 0.1.0
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: 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=l-gMVPP5X9m0IGqnjMjKPUo-olw4nf2cHvgz7DtdIjw,12746
3
+ busylib/exceptions.py,sha256=DHYoEXGdADXSxcgLq7BWqq_865J0LsMysZL-Wuip2wc,210
4
+ busylib/types.py,sha256=AGNd39jN5Rn5fI9kk5Ugk-4fr3JeXQpka2YEFeEWKNw,4223
5
+ busylib-0.1.0.dist-info/licenses/LICENSE,sha256=aJO9BQGuVb1fvGzHcgWYiwcC9kEFyD-FBb8SPO-ATJ4,1071
6
+ busylib-0.1.0.dist-info/METADATA,sha256=v8QccaMsA439sMQYg5hrQ9dAcDmIZKXuPHts6UnDIcc,5514
7
+ busylib-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ busylib-0.1.0.dist-info/top_level.txt,sha256=tXeqg2EVVj4E8-ywimkxRzuCJq2HqMXA0571IB15PDA,8
9
+ busylib-0.1.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,,