Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
091a53e
Bump minimum required Python version to 3.12 (#1871)
iMicknl Dec 29, 2025
fb20206
Bump version to 2.0
iMicknl Dec 29, 2025
d33beb1
Rename Scenario to ActionGroup (and relevant methods), reuse ActionGr…
iMicknl Dec 29, 2025
50f4a78
Add `execute_action_group` method and remove other command execution …
iMicknl Dec 29, 2025
ef4c4c5
Refactor authentication handling in OverkizClient and add new credent…
iMicknl Jan 3, 2026
f7e3cda
Migrate pre-commit to prek (rust based) (#1887)
iMicknl Jan 9, 2026
a04e726
Improve developer documentation (#1908)
iMicknl Jan 24, 2026
9d73c6a
Fix uv.lock after rebase
iMicknl Jan 25, 2026
faec9ae
Update test files to clarify ruff noqa comments and improve readabili…
iMicknl Jan 25, 2026
097af40
Implement ActionQueue for batching actions in OverkizClient (#1866)
iMicknl Jan 25, 2026
b3a8630
Redact
iMicknl Jan 26, 2026
0c8f666
Add UnknownEnumMixin for consistent handling of unknown enum values
iMicknl Jan 27, 2026
f2160c5
Refactor enums to use UnknownEnumMixin for consistent handling and im…
iMicknl Jan 27, 2026
a6306bf
Add methods for retrieving reference data in OverkizClient
iMicknl Jan 27, 2026
f208497
Add missing enum values and improve formatting in enums
iMicknl Jan 27, 2026
41c6f07
Enhance Protocol handling: add ProtocolType model, update Protocol en…
iMicknl Jan 27, 2026
5df60bb
Change return type of get_reference_ui_classes to list[str] for impro…
iMicknl Jan 27, 2026
dc1c99c
Update UI enums: enhance documentation, reorder values, and add missi…
iMicknl Jan 27, 2026
125d63d
Fix UIClass and UIWidget enum UNKNOWN value capitalization
iMicknl Jan 27, 2026
d36d26a
Add UIClassifier enum and update get_reference_ui_classifiers return …
iMicknl Jan 27, 2026
d63f079
feat: add UIProfile enum generation from Overkiz API
iMicknl Jan 27, 2026
87e5341
Apply suggestion from @Copilot
iMicknl Jan 27, 2026
d60fc03
fix: change return type of get_reference_ui_widgets to list[str] for …
iMicknl Jan 27, 2026
b8a8369
feat: add UIProfileDefinition model and related classes, and implemen…
iMicknl Jan 27, 2026
d032395
Address code review feedback: fix import pollution and remove redunda…
Copilot Jan 28, 2026
90641c9
Update utils/generate_enums.py
iMicknl Jan 28, 2026
564ebbf
Update pyoverkiz/enums/base.py
iMicknl Jan 28, 2026
b5e65b8
Explicitly acknowledge _missing_ override in UnknownEnumMixin (#1929)
Copilot Jan 29, 2026
2a7eb91
Bugfix
iMicknl Jan 29, 2026
d6fb955
Add command extraction and enum generation from fixture files
iMicknl Jan 29, 2026
16a8224
Add state value extraction and update OverkizCommandParam in enum gen…
iMicknl Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "uv sync && uv run pre-commit install",
"postCreateCommand": "uv sync --all-extras --dev && uv run prek install",
// Configure tool-specific properties.
"customizations": {
"vscode": {
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: docs

on:
release:
types:
- created
pull_request:
paths:
- docs/**
- mkdocs.yml
- pyproject.toml
- README.md

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Set up uv
uses: astral-sh/setup-uv@v3

- name: Install docs dependencies
run: uv sync --all-extras --dev

- name: Build site
run: uv run mkdocs build

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site

deploy:
if: github.event_name == 'release'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
6 changes: 3 additions & 3 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v6
Expand All @@ -32,7 +32,7 @@ jobs:
- name: Install the project
run: uv sync --all-extras --dev

- name: Run pre-commit
- name: Run prek (pre-commit checks)
env:
SKIP: ${{ github.ref == 'refs/heads/main' && 'no-commit-to-branch' || '' }}
run: uv run pre-commit run --show-diff-on-failure --color=always --all-files
run: uv run prek run --show-diff-on-failure --color=always --all-files
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v6
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
exclude: .devcontainer
exclude: .devcontainer|.vscode
- id: check-yaml
- id: check-added-large-files
- id: no-commit-to-branch
Expand Down
17 changes: 17 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Docs: Serve (development)",
"type": "shell",
"command": "uv run mkdocs serve"
},
{
"label": "Docs: Build",
"type": "shell",
"command": "uv run mkdocs build --clean"
},
]
}
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ Always use full type annotations, generics, and other modern practices.
# Install all dependencies:
uv sync

# Run linting (with ruff), pre-commit checks and type checking (with mypy).
# Run linting (with ruff), prek (pre-commit alternative) checks and type checking (with mypy).
# Note when you run this, ruff will auto-format and sort imports, resolving any
# linter warnings about import ordering:
uv run pre-commit run --all-files
uv run prek run --all-files

# Run tests:
uv run pytest
Expand Down
45 changes: 22 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,19 @@ pip install pyoverkiz
import asyncio
import time

from pyoverkiz.const import SUPPORTED_SERVERS
from pyoverkiz.auth.credentials import UsernamePasswordCredentials
from pyoverkiz.client import OverkizClient
from pyoverkiz.enums import Server
from pyoverkiz.models import Action
from pyoverkiz.enums import Server, OverkizCommand

USERNAME = ""
PASSWORD = ""


async def main() -> None:
async with OverkizClient(
USERNAME, PASSWORD, server=SUPPORTED_SERVERS[Server.SOMFY_EUROPE]
server=Server.SOMFY_EUROPE,
credentials=UsernamePasswordCredentials(USERNAME, PASSWORD),
) as client:
try:
await client.login()
Expand All @@ -61,6 +63,19 @@ async def main() -> None:
print(f"{device.label} ({device.id}) - {device.controllable_name}")
print(f"{device.widget} - {device.ui_class}")

await client.execute_action_group(
actions=[
Action(
device_url="io://1234-5678-1234/12345678",
commands=[
Command(name=OverkizCommand.SET_CLOSURE, parameters=[100])
]
)
],
label="Execution via Python",
# mode=CommandMode.HIGH_PRIORITY
)

while True:
events = await client.fetch_events()
print(events)
Expand All @@ -76,38 +91,22 @@ asyncio.run(main())
```python
import asyncio
import time
import aiohttp

from pyoverkiz.auth.credentials import LocalTokenCredentials
from pyoverkiz.client import OverkizClient
from pyoverkiz.const import SUPPORTED_SERVERS, OverkizServer
from pyoverkiz.enums import Server
from pyoverkiz.utils import create_local_server_config

USERNAME = ""
PASSWORD = ""
LOCAL_GATEWAY = "gateway-xxxx-xxxx-xxxx.local" # or use the IP address of your gateway
VERIFY_SSL = True # set verify_ssl to False if you don't use the .local hostname


async def main() -> None:
token = "" # generate your token via the Somfy app and include it here

# Local Connection
session = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=VERIFY_SSL)
)

async with OverkizClient(
username="",
password="",
token=token,
session=session,
server=create_local_server_config(host=LOCAL_GATEWAY),
credentials=LocalTokenCredentials(token),
verify_ssl=VERIFY_SSL,
server=OverkizServer(
name="Somfy TaHoma (local)",
endpoint=f"https://{LOCAL_GATEWAY}:8443/enduser-mobile-web/1/enduserAPI/",
manufacturer="Somfy",
configuration_url=None,
),
) as client:
await client.login()

Expand Down
147 changes: 147 additions & 0 deletions docs/action-queue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Action queue

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.

Important limitation:
- Gateways only allow a single action per device in each action group. The queue
merges commands for the same `device_url` into a single action to keep the
batch valid and preserve command order for that device.
- If you pass multiple actions for the same `device_url` in a single
`execute_action_group()` call, the queue will merge them for you.

## Enable with defaults

Set `action_queue=True` to enable batching with default settings:

```python
import asyncio

from pyoverkiz.auth import UsernamePasswordCredentials
from pyoverkiz.client import OverkizClient
from pyoverkiz.enums import OverkizCommand, Server
from pyoverkiz.models import Action, Command

client = OverkizClient(
server=Server.SOMFY_EUROPE,
credentials=UsernamePasswordCredentials("user@example.com", "password"),
action_queue=True, # uses defaults
)

action1 = Action(
device_url="io://1234-5678-1234/12345678",
commands=[Command(name=OverkizCommand.CLOSE)],
)
action2 = Action(
device_url="io://1234-5678-1234/87654321",
commands=[Command(name=OverkizCommand.OPEN)],
)

task1 = asyncio.create_task(client.execute_action_group([action1]))
task2 = asyncio.create_task(client.execute_action_group([action2]))
exec_id1, exec_id2 = await asyncio.gather(task1, task2)

print(exec_id1 == exec_id2)
```

Defaults:
- `delay=0.5`
- `max_actions=20`

## Advanced settings

If you need to tune batching behavior, pass `ActionQueueSettings`:

```python
import asyncio

from pyoverkiz.action_queue import ActionQueueSettings
from pyoverkiz.client import OverkizClient
from pyoverkiz.auth import UsernamePasswordCredentials
from pyoverkiz.enums import OverkizCommand, Server
from pyoverkiz.models import Action, Command

client = OverkizClient(
server=Server.SOMFY_EUROPE,
credentials=UsernamePasswordCredentials("user@example.com", "password"),
action_queue=ActionQueueSettings(
delay=0.5, # seconds to wait before auto-flush
max_actions=20, # auto-flush when this count is reached
),
)
```

## `flush_action_queue()` (force immediate execution)

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.

```python
from pyoverkiz.action_queue import ActionQueueSettings
import asyncio

from pyoverkiz.client import OverkizClient
from pyoverkiz.auth import UsernamePasswordCredentials
from pyoverkiz.enums import OverkizCommand, Server
from pyoverkiz.models import Action, Command

client = OverkizClient(
server=Server.SOMFY_EUROPE,
credentials=UsernamePasswordCredentials("user@example.com", "password"),
action_queue=ActionQueueSettings(delay=10.0), # long delay
)

action = Action(
device_url="io://1234-5678-1234/12345678",
commands=[Command(name=OverkizCommand.CLOSE)],
)

exec_task = asyncio.create_task(client.execute_action_group([action]))

# Give it time to enter the queue
await asyncio.sleep(0.05)

# Force immediate execution instead of waiting 10 seconds
await client.flush_action_queue()

exec_id = await exec_task
print(exec_id)
```

Why this matters:
- It lets you keep a long delay for batching, but still force a quick execution when a user interaction demands it.
- Useful before shutdown to avoid leaving actions waiting in the queue.

## `get_pending_actions_count()` (best-effort count)

`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.

```python
from pyoverkiz.client import OverkizClient
from pyoverkiz.auth import UsernamePasswordCredentials
from pyoverkiz.enums import OverkizCommand, Server
from pyoverkiz.models import Action, Command

client = OverkizClient(
server=Server.SOMFY_EUROPE,
credentials=UsernamePasswordCredentials("user@example.com", "password"),
action_queue=True,
)

action = Action(
device_url="io://1234-5678-1234/12345678",
commands=[Command(name=OverkizCommand.CLOSE)],
)

exec_task = asyncio.create_task(client.execute_action_group([action]))
await asyncio.sleep(0.01)

pending = client.get_pending_actions_count()
print(f"Pending actions (approx): {pending}")

exec_id = await exec_task
print(exec_id)
```

Why it’s best-effort:
- Actions may flush automatically while you read the count.
- New actions may be added concurrently by other tasks.
- The count can be briefly stale, so avoid using it to decide whether you must flush or not.
13 changes: 13 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
::: pyoverkiz.client.OverkizClient

::: pyoverkiz.models
options:
show_source: false

::: pyoverkiz.enums
options:
show_source: false

::: pyoverkiz.exceptions
options:
show_source: false
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading