Skip to content

Commit 097af40

Browse files
iMicknlCopilot
andcommitted
Implement ActionQueue for batching actions in OverkizClient (#1866)
Fixes #1865 --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: iMicknl <1424596+iMicknl@users.noreply.github.com>
1 parent faec9ae commit 097af40

File tree

8 files changed

+1120
-6
lines changed

8 files changed

+1120
-6
lines changed

docs/action-queue.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Action queue
2+
3+
The action queue automatically groups rapid, consecutive calls to `execute_action_group()` into a single ActionGroup execution. This minimizes the number of API calls and helps prevent rate limiting issues, such as `TooManyRequestsException`, `TooManyConcurrentRequestsException`, `TooManyExecutionsException`, or `ExecutionQueueFullException` which can occur if actions are sent individually in quick succession.
4+
5+
Important limitation:
6+
- Gateways only allow a single action per device in each action group. The queue
7+
merges commands for the same `device_url` into a single action to keep the
8+
batch valid and preserve command order for that device.
9+
- If you pass multiple actions for the same `device_url` in a single
10+
`execute_action_group()` call, the queue will merge them for you.
11+
12+
## Enable with defaults
13+
14+
Set `action_queue=True` to enable batching with default settings:
15+
16+
```python
17+
import asyncio
18+
19+
from pyoverkiz.auth import UsernamePasswordCredentials
20+
from pyoverkiz.client import OverkizClient
21+
from pyoverkiz.enums import OverkizCommand, Server
22+
from pyoverkiz.models import Action, Command
23+
24+
client = OverkizClient(
25+
server=Server.SOMFY_EUROPE,
26+
credentials=UsernamePasswordCredentials("user@example.com", "password"),
27+
action_queue=True, # uses defaults
28+
)
29+
30+
action1 = Action(
31+
device_url="io://1234-5678-1234/12345678",
32+
commands=[Command(name=OverkizCommand.CLOSE)],
33+
)
34+
action2 = Action(
35+
device_url="io://1234-5678-1234/87654321",
36+
commands=[Command(name=OverkizCommand.OPEN)],
37+
)
38+
39+
task1 = asyncio.create_task(client.execute_action_group([action1]))
40+
task2 = asyncio.create_task(client.execute_action_group([action2]))
41+
exec_id1, exec_id2 = await asyncio.gather(task1, task2)
42+
43+
print(exec_id1 == exec_id2)
44+
```
45+
46+
Defaults:
47+
- `delay=0.5`
48+
- `max_actions=20`
49+
50+
## Advanced settings
51+
52+
If you need to tune batching behavior, pass `ActionQueueSettings`:
53+
54+
```python
55+
import asyncio
56+
57+
from pyoverkiz.action_queue import ActionQueueSettings
58+
from pyoverkiz.client import OverkizClient
59+
from pyoverkiz.auth import UsernamePasswordCredentials
60+
from pyoverkiz.enums import OverkizCommand, Server
61+
from pyoverkiz.models import Action, Command
62+
63+
client = OverkizClient(
64+
server=Server.SOMFY_EUROPE,
65+
credentials=UsernamePasswordCredentials("user@example.com", "password"),
66+
action_queue=ActionQueueSettings(
67+
delay=0.5, # seconds to wait before auto-flush
68+
max_actions=20, # auto-flush when this count is reached
69+
),
70+
)
71+
```
72+
73+
## `flush_action_queue()` (force immediate execution)
74+
75+
Normally, queued actions are sent after the delay window or when `max_actions` is reached. Call `flush_action_queue()` to force the queue to execute immediately, which is useful when you want to send any pending actions without waiting for the delay timer to expire.
76+
77+
```python
78+
from pyoverkiz.action_queue import ActionQueueSettings
79+
import asyncio
80+
81+
from pyoverkiz.client import OverkizClient
82+
from pyoverkiz.auth import UsernamePasswordCredentials
83+
from pyoverkiz.enums import OverkizCommand, Server
84+
from pyoverkiz.models import Action, Command
85+
86+
client = OverkizClient(
87+
server=Server.SOMFY_EUROPE,
88+
credentials=UsernamePasswordCredentials("user@example.com", "password"),
89+
action_queue=ActionQueueSettings(delay=10.0), # long delay
90+
)
91+
92+
action = Action(
93+
device_url="io://1234-5678-1234/12345678",
94+
commands=[Command(name=OverkizCommand.CLOSE)],
95+
)
96+
97+
exec_task = asyncio.create_task(client.execute_action_group([action]))
98+
99+
# Give it time to enter the queue
100+
await asyncio.sleep(0.05)
101+
102+
# Force immediate execution instead of waiting 10 seconds
103+
await client.flush_action_queue()
104+
105+
exec_id = await exec_task
106+
print(exec_id)
107+
```
108+
109+
Why this matters:
110+
- It lets you keep a long delay for batching, but still force a quick execution when a user interaction demands it.
111+
- Useful before shutdown to avoid leaving actions waiting in the queue.
112+
113+
## `get_pending_actions_count()` (best-effort count)
114+
115+
`get_pending_actions_count()` returns a snapshot of how many actions are currently queued. Because the queue can change concurrently (and the method does not acquire the queue lock), the value is approximate. Use it for logging, diagnostics, or UI hints—not for critical control flow.
116+
117+
```python
118+
from pyoverkiz.client import OverkizClient
119+
from pyoverkiz.auth import UsernamePasswordCredentials
120+
from pyoverkiz.enums import OverkizCommand, Server
121+
from pyoverkiz.models import Action, Command
122+
123+
client = OverkizClient(
124+
server=Server.SOMFY_EUROPE,
125+
credentials=UsernamePasswordCredentials("user@example.com", "password"),
126+
action_queue=True,
127+
)
128+
129+
action = Action(
130+
device_url="io://1234-5678-1234/12345678",
131+
commands=[Command(name=OverkizCommand.CLOSE)],
132+
)
133+
134+
exec_task = asyncio.create_task(client.execute_action_group([action]))
135+
await asyncio.sleep(0.01)
136+
137+
pending = client.get_pending_actions_count()
138+
print(f"Pending actions (approx): {pending}")
139+
140+
exec_id = await exec_task
141+
print(exec_id)
142+
```
143+
144+
Why it’s best-effort:
145+
- Actions may flush automatically while you read the count.
146+
- New actions may be added concurrently by other tasks.
147+
- The count can be briefly stale, so avoid using it to decide whether you must flush or not.

docs/device-control.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,18 @@ await client.execute_action_group(
8585
],
8686
label="Execution: multiple commands",
8787
)
88-
```
8988

90-
## Polling vs event-driven
89+
## Limitations and rate limits
90+
91+
Gateways impose limits on how many executions can run or be queued simultaneously. If the execution queue is full, the API will raise an `ExecutionQueueFullException`. Most gateways allow up to 10 concurrent executions.
92+
93+
### Action queue (batching across calls)
94+
95+
If you trigger many action groups in rapid succession, you can enable the action
96+
queue to batch calls within a short window. This reduces API calls and helps
97+
avoid rate limits. See the [Action queue](action-queue.md) guide for setup,
98+
advanced settings, and usage patterns.
99+
100+
### Polling vs event-driven
91101

92102
Polling with `get_devices()` is straightforward but may introduce latency and increase server load. For more immediate updates, consider using event listeners. Refer to the [event handling](event-handling.md) guide for implementation details.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nav:
4646
- Getting started: getting-started.md
4747
- Core concepts: core-concepts.md
4848
- Device control: device-control.md
49+
- Action queue: action-queue.md
4950
- Event handling: event-handling.md
5051
- Error handling: error-handling.md
5152
- Troubleshooting: troubleshooting.md

0 commit comments

Comments
 (0)