Metadata-Version: 2.4
Name: pyatccala
Version: 0.2.0
Summary: Async client for the ATC Cala / Grupo Ferroli WiFi radiator cloud API
Author: belamadar
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://gitlab.com/belamadar/pyatccala
Project-URL: Repository, https://gitlab.com/belamadar/pyatccala
Project-URL: Issues, https://gitlab.com/belamadar/pyatccala/-/issues
Keywords: atc,cala,ferroli,radiator,heating,home-assistant
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Home Automation
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
Dynamic: license-file

# pyatccala

Async Python client for the **ATC Cala** / Grupo Ferroli WiFi radiator cloud API
(`gateway.grupoferroli.es`). It powers a companion ATC Cala Home Assistant integration but has no
Home Assistant dependency and can be used standalone.

The reverse-engineered protocol is documented in the companion integration repo's `API.md`.

## Install

```bash
pip install pyatccala
```

## Usage

```python
import asyncio
from pyatccala import AtcCalaClient, MODE_ECO

async def main():
    async with AtcCalaClient("you@example.com", "password") as client:
        await client.login()                      # OAuth password grant
        inst = (await client.get_installations())[0]

        for radiator in await client.get_radiators(inst.id_installation):
            print(radiator.name, radiator.mode, radiator.current_temperature,
                  radiator.target_temperature, radiator.boost_active)

        rad_id = radiator.id_radiator
        await client.async_set_mode(rad_id, MODE_ECO)
        await client.async_set_comfort_temperature(rad_id, 21.5)
        await client.async_set_target_temperature(radiator, 19)   # mode-aware
        await client.async_start_boost(inst.id_installation, rad_id, 22.0, 60)
        await client.async_stop_boost(rad_id)

        energy = await client.get_installation_consumption(inst.id_installation, "semanal")
        print(energy.total_kwh, energy.cost)

asyncio.run(main())
```

Every command helper accepts one radiator id or a list of ids, validates against the limits
the official app enforces (raising `ValueError`), and returns once the cloud has *queued* the
change — poll `get_radiators()` to see it applied.

### Things worth knowing about the protocol

- **Eco is an offset.** `TempEco` holds how many degrees (0.5–4.5) Eco sits *below* Comfort.
  `Radiator.temp_eco` is the resulting absolute temperature, `Radiator.eco_offset` the raw value;
  `async_set_eco_temperature()` converts for you.
- **Anti-frost is fixed** at 7 °C; Comfort is 12–30 °C; boost is 7–30 °C for up to ~1 year.
- **Boost** is an overlay (`ModoForzado` + `TiempoForzado` expiry) on top of the mode. The server
  leaves a stale `ModoForzado`/`TiempoForzado` behind, so use `Radiator.boost_active`.
- `TemperaturaConsignaCalculada` lags behind changes; `Radiator.target_temperature` resolves the
  effective setpoint locally (boost → mode → weekly schedule at the radiator's clock).

### API coverage

| Area | Methods |
|---|---|
| Installations | `get_installations`, `get_installation`, `async_add_installation`, `async_update_installation`, `async_delete_installation`, `async_clear_installation`, `get_server_time` |
| Zones | `get_zones`, `async_add_zone`, `async_rename_zone`, `async_delete_zones` |
| Radiators | `get_radiators`, `put_radiator_changes` (raw), `async_move_radiators`, `async_unpair_radiators`, `async_reset_radiators` |
| Radiator control | `async_set_power`, `async_set_mode`, `async_set_comfort_temperature`, `async_set_eco_temperature`, `async_set_eco_offset`, `async_set_target_temperature`, `async_start_boost`, `async_start_boost_until`, `async_set_boost_temperature`, `async_stop_boost`, `async_set_schedule` |
| Radiator settings | `async_set_keypad_lock`, `async_set_brightness`, `async_set_display_duration`, `async_set_power_limit`, `async_set_window_detection`, `async_clear_window_open`, `async_set_adaptive_start`, `async_set_easy_mode`, `async_set_probe_offset`, `async_set_name` |
| Energy | `get_installation_consumption`, `get_zone_consumption`, `get_radiator_consumption` (`diario` / `semanal` / `mensual` / `anual`) |
| Gateways | `get_gateways`, `async_add_gateway`, `async_unpair_gateway`, `async_unlink_gateway` |
| Account | `get_user`, `async_update_user`, `async_change_password`, `get_sessions`, `async_delete_session`, `async_logout` |
| Catalogs | `get_languages`, `get_currencies`, `get_timezones`, `get_device_types`, `get_legal_text` |

## Auth handling

`AtcCalaClient` manages the OAuth token transparently:

- The access token lasts ~20 minutes. `async_ensure_token()` refreshes it before expiry using the
  **refresh grant**, falling back to a full password login if the refresh is rejected.
- Any `401` from a data call triggers one automatic re-auth and retry.
- Errors are typed: `AtcCalaAuthError` (re-login required) vs `AtcCalaConnectionError` (transient).

Pass your own `aiohttp.ClientSession` via `session=` (recommended inside a larger app), or let the
client create and own one. `api_base=` can point at a mock/proxy for testing.

## Development

```bash
pip install -e .[test]
pytest -p no:homeassistant   # (the flag only matters if HA test plugins share the venv)
```
