Async-first Python client for the Danfoss Ally OpenAPI.
pip install pydanfossallyFor local development with Poetry and VS Code debugging, this repository is configured to use
an in-project virtual environment at .venv.
poetry install
poetry run python example.pyIf you already created a Poetry environment before this setting was added, recreate it once so
Poetry installs dependencies into .venv for this repository.
This is the recommended pattern for smart-home systems and other integrations that keep one client alive and reuse it across many calls.
from pydanfossally import DanfossAlly
ally = DanfossAlly(
timeout=30,
refresh_device_concurrency=5,
refresh_device_min_interval=0.10,
hot_refresh_timeout=300,
user_agent_prefix="HomeAssistant-DanfossAlly/2026.3.0",
)
authorized = await ally.initialize(key, secret)
if not authorized:
raise RuntimeError("Authorization failed")
devices = await ally.get_devices()
print(devices)
await ally.aclose()This is a good fit for small scripts, one-off tools, and examples where you want automatic resource cleanup.
import asyncio
import os
from pydanfossally import DanfossAlly
async def main() -> None:
async with DanfossAlly(
timeout=30,
refresh_device_concurrency=5,
refresh_device_min_interval=0.10,
hot_refresh_timeout=300,
user_agent_prefix="HomeAssistant-DanfossAlly/2026.3.0",
) as ally:
authorized = await ally.initialize(os.environ["KEY"], os.environ["SECRET"])
if not authorized:
raise RuntimeError("Authorization failed")
devices = await ally.get_devices()
print(devices)
asyncio.run(main())POST /oauth2/tokenGET /ally/devicesGET /ally/devices/{device_id}GET /ally/devices/{device_id}/sub-devicesGET /ally/devices/{device_id}/statusPOST /ally/devices/{device_id}/commands
The transport layer follows the OpenAPI file in docs/openapi-spec.
The parsed devices mapping is a best-effort convenience model built from observed status
codes. The OpenAPI file documents generic {code, value} pairs, but it does not define all
device-specific status or command codes. That means:
- request/response transport compatibility is covered by the library
- friendly parsed fields are based on current observed API behavior
- some status fields may vary between device types
- The OpenAPI file does not fully document which command
codevalues are supported for all device types. - The
POST /commandsresponse shape is inconsistent between schema and examples; this client accepts both{"result": true, "t": ...}and{"t": ...}. - Live verification should be performed against read-only endpoints before enabling write flows in production integrations.
Each refresh_devices() call always starts with a bulk read from GET /ally/devices.
After successful write operations (set_mode(...) and send_command(...)), the target device is
tracked as pending hot refresh. While pending, refresh_devices() also calls
GET /ally/devices/{device_id}/status for that device.
Hot refresh for a pending device stops when either of these conditions is met:
- the latest bulk snapshot differs from the baseline state captured when the write succeeded
- the hot refresh timeout is reached (default: 300 seconds / 5 minutes)
The refresh tuning knobs are configurable through DanfossAlly(...):
refresh_device_concurrencycontrols how many status hot refresh calls may run at oncerefresh_device_min_intervalcontrols the minimum delay in seconds between starting two status hot refresh callshot_refresh_timeoutcontrols how long a device stays in pending hot refresh mode
By default, the client sends a User-Agent header in the form pydanfossally/<version>.
Integrations can prepend their own identifier through user_agent_prefix, resulting in a final
header such as HomeAssistant-DanfossAlly/2026.3.0 pydanfossally/<version>.
The client includes explicit helpers for several writable temperature-like settings:
set_upper_temp(device_id, temp)set_lower_temp(device_id, temp)set_at_home_setting(device_id, temp)set_leaving_home_setting(device_id, temp)set_holiday_setting(device_id, temp)set_pause_setting(device_id, temp)
These helpers validate values before writing them:
- minimum
5.0 - maximum
35.0 - only
0.5degree steps
Example:
devices = await ally.get_devices()
device = devices["device-1"]
await ally.set_upper_temp("device-1", 28.0)
await ally.set_lower_temp("device-1", 7.0)
await ally.set_at_home_setting("device-1", 21.5)
await ally.set_leaving_home_setting("device-1", 17.0)
await ally.set_holiday_setting("device-1", 15.0)
await ally.set_pause_setting("device-1", 8.0)
await ally.refresh_device("device-1")
device = ally.devices["device-1"]
print(device["upper_temp"])
print(device["lower_temp"])
print(device["at_home_setting"])
print(device["leaving_home_setting"])
print(device["holiday_setting"])
print(device["pause_setting"])Read access for these values is exposed through the parsed device state returned by
get_devices(), get_device(), refresh_device(), and refresh_devices(). The library does
not currently include dedicated getter methods for these fields because they are already part of
the normal device model.
The repository includes example.py as a small async read-only example that uses credentials
from the environment.
