timberborn-http 0.0.2__tar.gz → 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: timberborn_http
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Timberborn http api wrapper
5
5
  Author-email: Agroqirax <124404386+Agroqirax@users.noreply.github.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -22,10 +22,9 @@ Supports both direct API control and webhook-based event handling.
22
22
 
23
23
  ## Features
24
24
 
25
- - **Control levers and adapters** in your Timberborn game
26
- - **Poll or watch** for state changes
27
- - **Webhook support** for real-time events
28
- - **Multiple interaction styles**: OOP, decorators, and direct function calls
25
+ - Get the state of levers & adapters from timberborn
26
+ - Update the state of levers
27
+ - Receive webhooks and trigger actions
29
28
 
30
29
  ## Installation
31
30
 
@@ -48,8 +47,8 @@ for lever in levers:
48
47
  print(lever.name, lever.state)
49
48
 
50
49
  # Control a lever
51
- api.switch_on("Main Water Pump")
52
- api.set_color("Main Water Pump", "ff0000")
50
+ api.switch_on("HTTP Lever 1")
51
+ api.set_color("HTTP Lever 1", "ff0000")
53
52
  ```
54
53
 
55
54
  ### 2. Webhook Events
@@ -57,30 +56,19 @@ api.set_color("Main Water Pump", "ff0000")
57
56
  ```python
58
57
  from timberborn_http import TimberbornWebhookServer
59
58
 
60
- server = TimberbornWebhookServer()
59
+ server = TimberbornWebhookServer(port=8081)
61
60
 
62
- @server.on("Main Water Pump")
61
+ @server.on_event("HTTP Lever 1")
63
62
  def handle_on(name):
64
63
  print(f"{name} turned ON!")
65
64
 
66
- server.start()
65
+ while True:
66
+ pass
67
67
  ```
68
68
 
69
- ## Documentation & Examples
70
-
71
- - [API Usage](/examples/api): Direct control and polling
72
- - [Webhook Usage](/examples/webhooks): Real-time event handling
69
+ More examples & details in the examples folder
73
70
 
74
71
  ## Getting Help
75
72
 
76
73
  - [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
77
- - [Discord](https://discord.gg/timberborn)
78
-
79
- ## Commands
80
-
81
- - `python -m venv .venv`
82
- - `pip install -r requirements.txt`
83
- - `pip install -e .`
84
- - `python -m build`
85
- - `python3 -m twine upload --repository testpypi dist/*`
86
- - `python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps timberborn_http`
74
+ - [Discord](https://discord.gg/timberborn) in `#⏱️automation` or `#🤖mod-creators`
@@ -0,0 +1,57 @@
1
+ # Timberborn HTTP API Wrapper
2
+
3
+ A Python library for interacting with the Timberborn HTTP automation API.
4
+ Supports both direct API control and webhook-based event handling.
5
+
6
+ ## Features
7
+
8
+ - Get the state of levers & adapters from timberborn
9
+ - Update the state of levers
10
+ - Receive webhooks and trigger actions
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install timberborn-http
16
+ ```
17
+
18
+ ## Quickstart
19
+
20
+ ### 1. Direct API Control
21
+
22
+ ```python
23
+ from timberborn_http import TimberbornAPI
24
+
25
+ api = TimberbornAPI("http://localhost:8080")
26
+
27
+ # Get all levers
28
+ levers = api.get_levers()
29
+ for lever in levers:
30
+ print(lever.name, lever.state)
31
+
32
+ # Control a lever
33
+ api.switch_on("HTTP Lever 1")
34
+ api.set_color("HTTP Lever 1", "ff0000")
35
+ ```
36
+
37
+ ### 2. Webhook Events
38
+
39
+ ```python
40
+ from timberborn_http import TimberbornWebhookServer
41
+
42
+ server = TimberbornWebhookServer(port=8081)
43
+
44
+ @server.on_event("HTTP Lever 1")
45
+ def handle_on(name):
46
+ print(f"{name} turned ON!")
47
+
48
+ while True:
49
+ pass
50
+ ```
51
+
52
+ More examples & details in the examples folder
53
+
54
+ ## Getting Help
55
+
56
+ - [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
57
+ - [Discord](https://discord.gg/timberborn) in `#⏱️automation` or `#🤖mod-creators`
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "timberborn_http"
10
- version = "0.0.2"
10
+ version = "0.0.4"
11
11
  authors = [
12
12
  { name="Agroqirax", email="124404386+Agroqirax@users.noreply.github.com" },
13
13
  ]
@@ -0,0 +1,14 @@
1
+ """
2
+ Timberborn HTTP
3
+ Basic usage:
4
+ ```
5
+ from timberborn_http import TimberbornAPI
6
+ api = TimberbornAPI("http://localhost:8080"
7
+ api.get_levers()
8
+ ```
9
+ :copyright: (c) 2026 by Agroqirax
10
+ :license: GNU GPL-3.0, see LICENSE for more details.
11
+ """
12
+
13
+ from ._client import TimberbornAPI, TimberbornWebhookServer
14
+ __all__ = ["TimberbornAPI", "TimberbornWebhookServer"]
@@ -0,0 +1,325 @@
1
+ from typing import Callable, Dict, List, Optional, overload
2
+ import logging
3
+ import threading
4
+ import urllib.parse
5
+
6
+ import requests
7
+ from flask import Flask, cli
8
+
9
+ logger = logging.getLogger("timberborn_http")
10
+
11
+
12
+ class TimberbornAPI:
13
+ """
14
+ Client for interacting with the Timberborn HTTP automation API.
15
+ """
16
+
17
+ def __init__(self, host: str = "http://localhost:8080"):
18
+ self.host = host.rstrip("/")
19
+
20
+ def _encode(self, name: str) -> str:
21
+ return urllib.parse.quote(name, safe="")
22
+
23
+ def _request(self, path: str) -> requests.Response:
24
+ url = f"{self.host}{path}"
25
+ try:
26
+ r = requests.get(url, timeout=5)
27
+ r.raise_for_status()
28
+ return r
29
+ except requests.exceptions.ConnectionError as e:
30
+ raise ConnectionError(
31
+ f"Failed to connect to Timberborn API at {self.host}. "
32
+ f"Is Timberborn running & the API started?"
33
+ ) from e
34
+ except requests.exceptions.Timeout as e:
35
+ raise TimeoutError(
36
+ f"Request to {url} timed out."
37
+ ) from e
38
+ except requests.exceptions.HTTPError as e:
39
+ raise RuntimeError(
40
+ f"HTTP error from Timberborn API: {r.status_code} {r.reason} — {url}"
41
+ ) from e
42
+
43
+ # ------------------------------------------------------------------
44
+ # Fetchers
45
+ # ------------------------------------------------------------------
46
+
47
+ def get_adapters(self) -> List["TimberbornAPI.Adapter"]:
48
+ """Retrieve all adapters."""
49
+ logger.debug("Fetching all adapters")
50
+ data = self._request("/api/adapters").json()
51
+ return [self.Adapter(self, d["name"]) for d in data]
52
+
53
+ def get_levers(self) -> List["TimberbornAPI.Lever"]:
54
+ """Retrieve all levers."""
55
+ logger.debug("Fetching all levers")
56
+ data = self._request("/api/levers").json()
57
+ return [self.Lever(self, d["name"]) for d in data]
58
+
59
+ def get_adapter(self, name: str) -> "TimberbornAPI.Adapter":
60
+ """Retrieve a single adapter by name."""
61
+ logger.debug("Fetching adapter '%s'", name)
62
+ # validates existence
63
+ self._request(f"/api/adapters/{self._encode(name)}")
64
+ return self.Adapter(self, name)
65
+
66
+ def get_lever(self, name: str) -> "TimberbornAPI.Lever":
67
+ """Retrieve a single lever by name."""
68
+ logger.debug("Fetching lever '%s'", name)
69
+ # validates existence
70
+ self._request(f"/api/levers/{self._encode(name)}")
71
+ return self.Lever(self, name)
72
+
73
+ # ------------------------------------------------------------------
74
+ # Low-level control (used by Adapter/Lever, but also callable directly)
75
+ # ------------------------------------------------------------------
76
+
77
+ def get_adapter_state(self, name: str) -> bool:
78
+ """Fetch the live state of an adapter."""
79
+ data = self._request(f"/api/adapters/{self._encode(name)}").json()
80
+ return bool(data["state"])
81
+
82
+ def get_lever_state(self, name: str) -> bool:
83
+ """Fetch the live state of a lever."""
84
+ data = self._request(f"/api/levers/{self._encode(name)}").json()
85
+ return bool(data["state"])
86
+
87
+ def get_lever_spring_return(self, name: str) -> bool:
88
+ """Fetch the live springReturn value of a lever."""
89
+ data = self._request(f"/api/levers/{self._encode(name)}").json()
90
+ return bool(data["springReturn"])
91
+
92
+ def switch_on(self, name: str) -> None:
93
+ """Switch a lever ON."""
94
+ logger.info("Switching ON lever '%s'", name)
95
+ self._request(f"/api/switch-on/{self._encode(name)}")
96
+
97
+ def switch_off(self, name: str) -> None:
98
+ """Switch a lever OFF."""
99
+ logger.info("Switching OFF lever '%s'", name)
100
+ self._request(f"/api/switch-off/{self._encode(name)}")
101
+
102
+ def toggle(self, name: str) -> None:
103
+ """Toggle a lever between ON and OFF."""
104
+ logger.info("Toggling lever '%s'", name)
105
+ if self.get_lever_state(name):
106
+ self.switch_off(name)
107
+ else:
108
+ self.switch_on(name)
109
+
110
+ def set_color(self, name: str, hex_color: str) -> None:
111
+ """
112
+ Set lever color.
113
+
114
+ Parameters
115
+ ----------
116
+ hex_color : str
117
+ Hex color without '#' (e.g. '00ff00').
118
+ """
119
+ hex_color = hex_color.lstrip("#")
120
+ if len(hex_color) != 6 or not all(c in "0123456789abcdefABCDEF" for c in hex_color):
121
+ raise ValueError(
122
+ f"Invalid hex color: '{hex_color}'. Expected 6 hex digits.")
123
+ logger.info("Setting color of '%s' to #%s", name, hex_color)
124
+ self._request(f"/api/color/{self._encode(name)}/{hex_color}")
125
+
126
+ # ------------------------------------------------------------------
127
+ # Inner classes
128
+ # ------------------------------------------------------------------
129
+
130
+ class Adapter:
131
+ """
132
+ Represents a Timberborn HTTP Adapter block.
133
+
134
+ Attributes
135
+ ----------
136
+ name : str
137
+ Name of the adapter block in-game.
138
+ state : bool
139
+ Live-fetched state of the adapter (True = ON). Calls the API on access.
140
+ """
141
+
142
+ def __init__(self, api: "TimberbornAPI", name: str):
143
+ self._api = api
144
+ self.name = name
145
+
146
+ @property
147
+ def state(self) -> bool:
148
+ """Live state — fetches from API on every access."""
149
+ return self._api.get_adapter_state(self.name)
150
+
151
+ def __repr__(self) -> str:
152
+ return f"Adapter(name={self.name!r})"
153
+
154
+ class Lever:
155
+ """
156
+ Represents a Timberborn HTTP Lever block.
157
+
158
+ Attributes
159
+ ----------
160
+ name : str
161
+ Name of the lever block in-game.
162
+ state : bool
163
+ Live-fetched state of the lever (True = ON). Calls the API on access.
164
+ springReturn : bool
165
+ Live-fetched springReturn flag. Calls the API on access.
166
+ """
167
+
168
+ def __init__(self, api: "TimberbornAPI", name: str):
169
+ self._api = api
170
+ self.name = name
171
+
172
+ @property
173
+ def state(self) -> bool:
174
+ """Live state — fetches from API on every access."""
175
+ return self._api.get_lever_state(self.name)
176
+
177
+ @property
178
+ def spring_return(self) -> bool:
179
+ """Live springReturn — fetches from API on every access."""
180
+ return self._api.get_lever_spring_return(self.name)
181
+
182
+ def switch_on(self) -> None:
183
+ """Switch this lever ON."""
184
+ self._api.switch_on(self.name)
185
+
186
+ def switch_off(self) -> None:
187
+ """Switch this lever OFF."""
188
+ self._api.switch_off(self.name)
189
+
190
+ def toggle(self) -> None:
191
+ """Toggle this lever."""
192
+ self._api.toggle(self.name)
193
+
194
+ def set_color(self, hex_color: str) -> None:
195
+ """Set the lever color (hex without '#', e.g. 'ff0000')."""
196
+ self._api.set_color(self.name, hex_color)
197
+
198
+ def __repr__(self) -> str:
199
+ return f"Lever(name={self.name!r})"
200
+
201
+
202
+ class TimberbornWebhookServer:
203
+ """
204
+ Webhook server that receives adapter events from Timberborn.
205
+ """
206
+
207
+ def __init__(self, host: str = "0.0.0.0", port: int = 8081):
208
+ """
209
+ Create a webhook server.
210
+
211
+ Parameters
212
+ ----------
213
+ host : str
214
+ Host to bind to.
215
+ port : int
216
+ Port to listen on.
217
+ """
218
+
219
+ self.host = host
220
+ self.port = port
221
+
222
+ self.app = Flask("timberborn_webhooks")
223
+
224
+ self.on_callbacks: Dict[str, Callable[[str], None]] = {}
225
+ self.off_callbacks: Dict[str, Callable[[str], None]] = {}
226
+
227
+ self.app.add_url_rule("/on/<name>", "on_event",
228
+ lambda name: self._event("ON", name),
229
+ methods=["GET", "POST"])
230
+
231
+ self.app.add_url_rule("/off/<name>", "off_event",
232
+ lambda name: self._event("OFF", name),
233
+ methods=["GET", "POST"])
234
+
235
+ self.start()
236
+
237
+ def _event(self, state: str, name: str):
238
+ name = urllib.parse.unquote(name)
239
+
240
+ logger.info("Adapter '%s' turned %s", name, state)
241
+
242
+ callbacks = self.on_callbacks if state == "ON" else self.off_callbacks
243
+
244
+ if name in callbacks:
245
+ callbacks[name](name)
246
+
247
+ return "OK"
248
+
249
+ @overload
250
+ def on_event(self, adapter_name: str) -> Callable[[
251
+ Callable[[str], None]], Callable[[str], None]]: ...
252
+
253
+ @overload
254
+ def on_event(self, adapter_name: str,
255
+ func: Callable[[str], None]) -> None: ...
256
+
257
+ def on_event(
258
+ self,
259
+ adapter_name: str,
260
+ func: Optional[Callable[[str], None]] = None
261
+ ) -> Optional[Callable[[Callable[[str], None]], Callable[[str], None]]]:
262
+ """
263
+ Register a callback when an adapter turns ON.
264
+
265
+ Can be used as a decorator or function call.
266
+ """
267
+
268
+ if func is None:
269
+ def decorator(f: Callable[[str], None]) -> Callable[[str], None]:
270
+ self.on_callbacks[adapter_name] = f
271
+ return f
272
+
273
+ return decorator
274
+
275
+ self.on_callbacks[adapter_name] = func
276
+ return None
277
+
278
+ @overload
279
+ def off_event(self, adapter_name: str) -> Callable[[
280
+ Callable[[str], None]], Callable[[str], None]]: ...
281
+
282
+ @overload
283
+ def off_event(self, adapter_name: str,
284
+ func: Callable[[str], None]) -> None: ...
285
+
286
+ def off_event(
287
+ self,
288
+ adapter_name: str,
289
+ func: Optional[Callable[[str], None]] = None
290
+ ) -> Optional[Callable[[Callable[[str], None]], Callable[[str], None]]]:
291
+ """
292
+ Register a callback when an adapter turns OFF.
293
+
294
+ Can be used as a decorator or function call.
295
+ """
296
+
297
+ if func is None:
298
+ def decorator(f: Callable[[str], None]) -> Callable[[str], None]:
299
+ self.off_callbacks[adapter_name] = f
300
+ return f
301
+
302
+ return decorator
303
+
304
+ self.off_callbacks[adapter_name] = func
305
+ return None
306
+
307
+ def start(self) -> None:
308
+ """Start webhook server. Can take a second or two to start."""
309
+ logger.info("Starting webhook server on %s:%s", self.host, self.port)
310
+
311
+ cli.show_server_banner = lambda *x: None
312
+
313
+ logging.getLogger("werkzeug").setLevel(logging.ERROR)
314
+
315
+ thread = threading.Thread(
316
+ target=lambda: self.app.run(
317
+ host=self.host,
318
+ port=self.port,
319
+ debug=False,
320
+ use_reloader=False
321
+ ),
322
+ daemon=True,
323
+ )
324
+
325
+ thread.start()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: timberborn_http
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Timberborn http api wrapper
5
5
  Author-email: Agroqirax <124404386+Agroqirax@users.noreply.github.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -22,10 +22,9 @@ Supports both direct API control and webhook-based event handling.
22
22
 
23
23
  ## Features
24
24
 
25
- - **Control levers and adapters** in your Timberborn game
26
- - **Poll or watch** for state changes
27
- - **Webhook support** for real-time events
28
- - **Multiple interaction styles**: OOP, decorators, and direct function calls
25
+ - Get the state of levers & adapters from timberborn
26
+ - Update the state of levers
27
+ - Receive webhooks and trigger actions
29
28
 
30
29
  ## Installation
31
30
 
@@ -48,8 +47,8 @@ for lever in levers:
48
47
  print(lever.name, lever.state)
49
48
 
50
49
  # Control a lever
51
- api.switch_on("Main Water Pump")
52
- api.set_color("Main Water Pump", "ff0000")
50
+ api.switch_on("HTTP Lever 1")
51
+ api.set_color("HTTP Lever 1", "ff0000")
53
52
  ```
54
53
 
55
54
  ### 2. Webhook Events
@@ -57,30 +56,19 @@ api.set_color("Main Water Pump", "ff0000")
57
56
  ```python
58
57
  from timberborn_http import TimberbornWebhookServer
59
58
 
60
- server = TimberbornWebhookServer()
59
+ server = TimberbornWebhookServer(port=8081)
61
60
 
62
- @server.on("Main Water Pump")
61
+ @server.on_event("HTTP Lever 1")
63
62
  def handle_on(name):
64
63
  print(f"{name} turned ON!")
65
64
 
66
- server.start()
65
+ while True:
66
+ pass
67
67
  ```
68
68
 
69
- ## Documentation & Examples
70
-
71
- - [API Usage](/examples/api): Direct control and polling
72
- - [Webhook Usage](/examples/webhooks): Real-time event handling
69
+ More examples & details in the examples folder
73
70
 
74
71
  ## Getting Help
75
72
 
76
73
  - [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
77
- - [Discord](https://discord.gg/timberborn)
78
-
79
- ## Commands
80
-
81
- - `python -m venv .venv`
82
- - `pip install -r requirements.txt`
83
- - `pip install -e .`
84
- - `python -m build`
85
- - `python3 -m twine upload --repository testpypi dist/*`
86
- - `python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps timberborn_http`
74
+ - [Discord](https://discord.gg/timberborn) in `#⏱️automation` or `#🤖mod-creators`
@@ -1,69 +0,0 @@
1
- # Timberborn HTTP API Wrapper
2
-
3
- A Python library for interacting with the Timberborn HTTP automation API.
4
- Supports both direct API control and webhook-based event handling.
5
-
6
- ## Features
7
-
8
- - **Control levers and adapters** in your Timberborn game
9
- - **Poll or watch** for state changes
10
- - **Webhook support** for real-time events
11
- - **Multiple interaction styles**: OOP, decorators, and direct function calls
12
-
13
- ## Installation
14
-
15
- ```bash
16
- pip install timberborn-http
17
- ```
18
-
19
- ## Quickstart
20
-
21
- ### 1. Direct API Control
22
-
23
- ```python
24
- from timberborn_http import TimberbornAPI
25
-
26
- api = TimberbornAPI("http://localhost:8080")
27
-
28
- # Get all levers
29
- levers = api.get_levers()
30
- for lever in levers:
31
- print(lever.name, lever.state)
32
-
33
- # Control a lever
34
- api.switch_on("Main Water Pump")
35
- api.set_color("Main Water Pump", "ff0000")
36
- ```
37
-
38
- ### 2. Webhook Events
39
-
40
- ```python
41
- from timberborn_http import TimberbornWebhookServer
42
-
43
- server = TimberbornWebhookServer()
44
-
45
- @server.on("Main Water Pump")
46
- def handle_on(name):
47
- print(f"{name} turned ON!")
48
-
49
- server.start()
50
- ```
51
-
52
- ## Documentation & Examples
53
-
54
- - [API Usage](/examples/api): Direct control and polling
55
- - [Webhook Usage](/examples/webhooks): Real-time event handling
56
-
57
- ## Getting Help
58
-
59
- - [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
60
- - [Discord](https://discord.gg/timberborn)
61
-
62
- ## Commands
63
-
64
- - `python -m venv .venv`
65
- - `pip install -r requirements.txt`
66
- - `pip install -e .`
67
- - `python -m build`
68
- - `python3 -m twine upload --repository testpypi dist/*`
69
- - `python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps timberborn_http`
@@ -1,2 +0,0 @@
1
- from ._client import TimberbornAPI, TimberbornWebhookServer, Adapter, Lever
2
- __all__ = ["TimberbornAPI", "TimberbornWebhookServer", "Adapter", "Lever"]
@@ -1,417 +0,0 @@
1
- from dataclasses import dataclass, field
2
- from typing import Callable, Dict, List, Optional
3
- import logging
4
- import threading
5
- import time
6
- import urllib.parse
7
-
8
- import requests
9
- from flask import Flask
10
-
11
- logger = logging.getLogger("timberborn_http")
12
-
13
-
14
- # ============================================================
15
- # Data Classes
16
- # ============================================================
17
-
18
-
19
- @dataclass
20
- class Adapter:
21
- """
22
- Represents a Timberborn HTTP Adapter block.
23
-
24
- Attributes
25
- ----------
26
- name : str
27
- Name of the adapter block in-game.
28
- state : bool
29
- Current state of the adapter (True = ON).
30
- """
31
-
32
- name: str
33
- state: bool
34
-
35
-
36
- @dataclass
37
- class Lever:
38
- """
39
- Represents a Timberborn HTTP Lever block.
40
-
41
- Attributes
42
- ----------
43
- name : str
44
- Name of the lever block in-game.
45
- state : bool
46
- Current state of the lever (True = ON).
47
- springReturn : bool
48
- Whether the lever automatically resets.
49
- api : TimberbornAPI
50
- Reference to the API client used for control.
51
- """
52
-
53
- name: str
54
- state: bool
55
- springReturn: bool
56
- api: "TimberbornAPI" = field(repr=False)
57
-
58
- # ------------------------------------------
59
-
60
- def on(self) -> None:
61
- """Switch this lever ON."""
62
- self.api.switch_on(self.name)
63
-
64
- def off(self) -> None:
65
- """Switch this lever OFF."""
66
- self.api.switch_off(self.name)
67
-
68
- def toggle(self) -> None:
69
- """Toggle this lever."""
70
- self.api.toggle(self.name)
71
-
72
- def set_color(self, hex_color: str) -> None:
73
- """
74
- Set the lever color.
75
-
76
- Parameters
77
- ----------
78
- hex_color : str
79
- Hex color code (e.g. 'ff0000').
80
- """
81
- self.api.set_color(self.name, hex_color)
82
-
83
- def refresh(self) -> None:
84
- """Refresh the lever state from the API."""
85
- self.state = self.api.get_lever_state(self.name)
86
-
87
-
88
- # ============================================================
89
- # API Client
90
- # ============================================================
91
-
92
-
93
- class TimberbornAPI:
94
- """
95
- Client for interacting with the Timberborn HTTP automation API.
96
- """
97
-
98
- def __init__(self, host: str = "http://localhost:8080"):
99
- """
100
- Initialize the API client.
101
-
102
- Parameters
103
- ----------
104
- host : str
105
- Base URL of the Timberborn API.
106
- """
107
- self.host = host.rstrip("/")
108
-
109
- # ------------------------------------------
110
-
111
- def _encode(self, name: str) -> str:
112
- """URL-encode a block name."""
113
- return urllib.parse.quote(name)
114
-
115
- # ------------------------------------------
116
- # Query
117
- # ------------------------------------------
118
-
119
- def get_adapters(self) -> List[Adapter]:
120
- """
121
- Retrieve all adapters.
122
-
123
- Returns
124
- -------
125
- List[Adapter]
126
- List of adapters.
127
- """
128
- logger.debug("Fetching adapters")
129
-
130
- r = requests.get(f"{self.host}/api/adapters")
131
- r.raise_for_status()
132
-
133
- data = r.json()
134
-
135
- return [Adapter(**adapter) for adapter in data]
136
-
137
- def get_levers(self) -> List[Lever]:
138
- """
139
- Retrieve all levers.
140
-
141
- Returns
142
- -------
143
- List[Lever]
144
- List of lever objects.
145
- """
146
- logger.debug("Fetching levers")
147
-
148
- r = requests.get(f"{self.host}/api/levers")
149
- r.raise_for_status()
150
-
151
- data = r.json()
152
-
153
- return [Lever(**lever, api=self) for lever in data]
154
-
155
- # ------------------------------------------
156
- # State helpers
157
- # ------------------------------------------
158
-
159
- def get_adapter_state(self, name: str) -> bool:
160
- """
161
- Get adapter state.
162
-
163
- Parameters
164
- ----------
165
- name : str
166
- Adapter name.
167
-
168
- Returns
169
- -------
170
- bool
171
- Adapter state.
172
- """
173
- for adapter in self.get_adapters():
174
- if adapter.name == name:
175
- return adapter.state
176
-
177
- raise ValueError(f"Adapter '{name}' not found")
178
-
179
- def get_lever_state(self, name: str) -> bool:
180
- """
181
- Get lever state.
182
-
183
- Parameters
184
- ----------
185
- name : str
186
- Lever name.
187
-
188
- Returns
189
- -------
190
- bool
191
- Lever state.
192
- """
193
- for lever in self.get_levers():
194
- if lever.name == name:
195
- return lever.state
196
-
197
- raise ValueError(f"Lever '{name}' not found")
198
-
199
- # ------------------------------------------
200
- # Control
201
- # ------------------------------------------
202
-
203
- def switch_on(self, name: str) -> None:
204
- """
205
- Switch a lever ON.
206
-
207
- Parameters
208
- ----------
209
- name : str
210
- Lever name.
211
- """
212
- logger.info("Switching ON lever '%s'", name)
213
-
214
- name = self._encode(name)
215
- requests.get(f"{self.host}/api/switch-on/{name}").raise_for_status()
216
-
217
- def switch_off(self, name: str) -> None:
218
- """
219
- Switch a lever OFF.
220
-
221
- Parameters
222
- ----------
223
- name : str
224
- Lever name.
225
- """
226
- logger.info("Switching OFF lever '%s'", name)
227
-
228
- name = self._encode(name)
229
- requests.get(f"{self.host}/api/switch-off/{name}").raise_for_status()
230
-
231
- def toggle(self, name: str) -> None:
232
- """
233
- Toggle a lever state.
234
-
235
- Parameters
236
- ----------
237
- name : str
238
- Lever name.
239
- """
240
- logger.info("Toggling lever '%s'", name)
241
-
242
- if self.get_lever_state(name):
243
- self.switch_off(name)
244
- else:
245
- self.switch_on(name)
246
-
247
- def set_color(self, name: str, hex_color: str) -> None:
248
- """
249
- Set lever color.
250
-
251
- Parameters
252
- ----------
253
- name : str
254
- Lever name.
255
- hex_color : str
256
- Hex color (e.g. '00ff00').
257
- """
258
- logger.info("Setting color of '%s' to %s", name, hex_color)
259
-
260
- name = self._encode(name)
261
- requests.get(
262
- f"{self.host}/api/color/{name}/{hex_color}"
263
- ).raise_for_status()
264
-
265
- # ------------------------------------------
266
- # Polling Watcher
267
- # ------------------------------------------
268
-
269
- def watch_adapter(
270
- self,
271
- name: str,
272
- callback: Callable[[str, bool], None],
273
- poll_interval: float = 1.0,
274
- ) -> None:
275
- """
276
- Watch an adapter and trigger a callback when its state changes.
277
-
278
- Parameters
279
- ----------
280
- name : str
281
- Adapter name.
282
- callback : Callable[[str, bool], None]
283
- Function called with (name, state).
284
- poll_interval : float
285
- Seconds between polls.
286
- """
287
-
288
- def watcher():
289
- logger.info("Watching adapter '%s'", name)
290
-
291
- last_state: Optional[bool] = None
292
-
293
- while True:
294
- state = self.get_adapter_state(name)
295
-
296
- if last_state is None:
297
- last_state = state
298
-
299
- if state != last_state:
300
- callback(name, state)
301
- last_state = state
302
-
303
- time.sleep(poll_interval)
304
-
305
- thread = threading.Thread(target=watcher, daemon=True)
306
- thread.start()
307
-
308
-
309
- # ============================================================
310
- # Webhook Server
311
- # ============================================================
312
-
313
-
314
- class TimberbornWebhookServer:
315
- """
316
- Webhook server that receives adapter events from Timberborn.
317
- """
318
-
319
- def __init__(self, host: str = "0.0.0.0", port: int = 8081):
320
- """
321
- Create a webhook server.
322
-
323
- Parameters
324
- ----------
325
- host : str
326
- Host to bind to.
327
- port : int
328
- Port to listen on.
329
- """
330
-
331
- self.host = host
332
- self.port = port
333
-
334
- self.app = Flask("timberborn_webhooks")
335
-
336
- self.on_callbacks: Dict[str, Callable[[str], None]] = {}
337
- self.off_callbacks: Dict[str, Callable[[str], None]] = {}
338
-
339
- self.app.add_url_rule("/on/<name>", "on_event",
340
- self._on_event, methods=["GET", "POST"])
341
- self.app.add_url_rule("/off/<name>", "off_event",
342
- self._off_event, methods=["GET", "POST"])
343
-
344
- # ------------------------------------------
345
-
346
- def _on_event(self, name: str) -> str:
347
- name = urllib.parse.unquote(name)
348
-
349
- logger.info("Adapter '%s' turned ON", name)
350
-
351
- if name in self.on_callbacks:
352
- self.on_callbacks[name](name)
353
-
354
- return "OK"
355
-
356
- def _off_event(self, name: str) -> str:
357
- name = urllib.parse.unquote(name)
358
-
359
- logger.info("Adapter '%s' turned OFF", name)
360
-
361
- if name in self.off_callbacks:
362
- self.off_callbacks[name](name)
363
-
364
- return "OK"
365
-
366
- # ------------------------------------------
367
- # Registration
368
- # ------------------------------------------
369
-
370
- def on(self, adapter_name: str, func: Optional[Callable[[str], None]] = None):
371
- """
372
- Register a callback when an adapter turns ON.
373
-
374
- Can be used as a decorator or function call.
375
- """
376
-
377
- if func is None:
378
-
379
- def decorator(f):
380
- self.on_callbacks[adapter_name] = f
381
- return f
382
-
383
- return decorator
384
-
385
- self.on_callbacks[adapter_name] = func
386
-
387
- def off(self, adapter_name: str, func: Optional[Callable[[str], None]] = None):
388
- """
389
- Register a callback when an adapter turns OFF.
390
-
391
- Can be used as a decorator or function call.
392
- """
393
-
394
- if func is None:
395
-
396
- def decorator(f):
397
- self.off_callbacks[adapter_name] = f
398
- return f
399
-
400
- return decorator
401
-
402
- self.off_callbacks[adapter_name] = func
403
-
404
- # ------------------------------------------
405
-
406
- def start(self) -> None:
407
- """Start the webhook server in a background thread."""
408
-
409
- logger.info("Starting webhook server on %s:%s", self.host, self.port)
410
-
411
- thread = threading.Thread(
412
- target=self.app.run,
413
- kwargs={"host": self.host, "port": self.port},
414
- daemon=True,
415
- )
416
-
417
- thread.start()
File without changes