# aiocamedomotic > Async Python library for interacting with CAME Domotic home automation systems. Provides automatic session management and device control. ## Overview The **CAME Domotic Library** ([aiocamedomotic](https://github.com/camedomotic-unofficial/aiocamedomotic)) provides a streamlined Python interface for interacting with CAME Domotic plants, much like the official [CAME Domotic app](https://www.came.com/global/itex/installers/solutions/domotica-e-termoregolazione/prodotti-compatibili-domotica/app-domotic-30). This library is designed to simplify the management of domotic devices by abstracting the complexities of the CAME Domotic API. Although primarily developed for use with [Home Assistant](https://www.home-assistant.io/), the library is freely available under the [Apache license 2.0](http://www.apache.org/licenses/LICENSE-2.0) for anyone interested in experimenting with a CAME Domotic plant. **IMPORTANT:** This library is independently developed and is not affiliated with, endorsed by, or supported by [CAME](https://www.came.com/). It may not be compatible with all CAME Domotic systems. While this library is stable and publicly released, it comes with no guarantees. Use at your own risk. **DANGER:** This library is not intended for use in critical systems, such as security or life-support systems. Always use official and supported tools for such applications. ### Key Features - **Simplicity**: Easy interaction with domotic entities. - **Automatic session management**: No need for manual login or session handling. - **Real-time updates**: Built-in long-polling support with typed update classes for monitoring device state changes as they happen. - **First of its kind**: Unique in providing integration with CAME Domotic systems. - **Open source**: Freely available under the Apache 2.0 license, inviting contributions and adaptations. ## Getting started This guide will walk you through the steps to quickly start using the CAME Domotic library in your projects. Before you begin, ensure you have: - Python 3.12, 3.13 or 3.14 installed on your system. - Access to a CAME Domotic server. ### Installation Use [pip](https://pip.pypa.io/en/stable/) to install the latest version of the CAME Domotic library and its dependencies: ```bash pip install aiocamedomotic ``` ### Basic usage examples Here’s a simple example to demonstrate how to use the library to turn on or off a light: ```python import asyncio from aiocamedomotic import CameDomoticAPI from aiocamedomotic.models import LightStatus async def main(): async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: # Get the server info server_info = await api.async_get_server_info() print(f"Keycode: {server_info.keycode}") # Get the list of all the lights configured on the CAME Domotic server lights = await api.async_get_lights() # Get a specific light by ID bedroom_dimmable_lamp = next((l for l in lights if l.act_id == 33), None) # Get a specific light by name kitchen_lamp = next( (l for l in lights if l.name == "My beautiful lamp"), None ) # Ensure the light is found (dimmable) if bedroom_dimmable_lamp: # Turn the light on, setting the brightness to 50% await bedroom_dimmable_lamp.async_set_status(LightStatus.ON, brightness=50) # Turn the light off await bedroom_dimmable_lamp.async_set_status(LightStatus.OFF) # Turn the light on, leaving the brightness unchanged await bedroom_dimmable_lamp.async_set_status(LightStatus.ON) # Ensure the light is found if kitchen_lamp: # Turn the light on await kitchen_lamp.async_set_status(LightStatus.ON) # Turn the light off await kitchen_lamp.async_set_status(LightStatus.OFF) # Run the main function asyncio.run(main()) ``` Let’s go step by step: 1. **Creating a Server Instance**: First, import the `CameDomoticAPI` classes from the library and create a `CameDomoticAPI` instance using the async factory method `CameDomoticAPI.async_create`. ```python import asyncio from aiocamedomotic import CameDomoticAPI from aiocamedomotic.models import LightStatus async def main(): async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: ... ``` This command will raise a `CameDomoticServerNotFoundError` exception if the server is not found (typically, bad IP/hostname or other network issue). Notice that the `CameDomoticAPI` class is an asynchronous context manager, so it must be used with the `async with` statement. #### NOTE The session is *NOT* authenticated at this point: the library will authenticate only when the first actual call to the server is made. In case the provided credentials are not valid, a `CameDomoticAuthError` exception will be raised at that time. 2. **Getting the server info**: You can retrieve the server info (keycode, serial number, etc.) by using the awaitable method `api.async_get_server_info()`. ```python # Get the server info server_info = await api.async_get_server_info() print(f"Keycode: {server_info.keycode}") ``` Since this is the first actual call to the server, the library will now authenticate: if the provided credentials are not valid, a `CameDomoticAuthError` exception will be raised. 3. **Fetching the list of available lights**: You can retrieve a list of all the lights configured on the CAME Domotic server by using the awaitable method `api.async_get_lights()`. ```python # Get the list of all the lights configured on the CAME Domotic server lights = await api.async_get_lights() ``` 4. **Selecting a specific light**: You can select a specific light, for example, by ID (`act_id` attribute) or display name (`name` attribute): ```python # Get a specific light by ID bedroom_dimmable_lamp = next((l for l in lights if l.act_id == 33), None) # Get a specific light by name kitchen_lamp = next( (l for l in lights if l.name == "My beautiful lamp"), None ) ``` 5. **Changing the status of a light** Lights are controlled by the method `async_set_status`. You can turn a light on or off by passing respectively the status `LightStatus.ON` or `LightStatus.OFF` as an argument. You can also set the brightness level of a dimmable light by passing the optional `brightness` argument (range: 0-100). ```python # Ensure the light is found (dimmable) if bedroom_dimmable_lamp: # Turn the light on, setting the brightness to 50% await bedroom_dimmable_lamp.async_set_status(LightStatus.ON, brightness=50) # Turn the light off await bedroom_dimmable_lamp.async_set_status(LightStatus.OFF) # Turn the light on, leaving the brightness unchanged await bedroom_dimmable_lamp.async_set_status(LightStatus.ON) # Ensure the light is found if kitchen_lamp: # Turn the light on await kitchen_lamp.async_set_status(LightStatus.ON) # Turn the light off await kitchen_lamp.async_set_status(LightStatus.OFF) ``` Congratulations! You’ve successfully used the CAME Domotic library to interact with your CAME Domotic server. ### Exploring further - For more detailed examples see [Usage examples](#usage-examples). - To check the technical specifications see the [API Reference](#api-reference). Thank you for choosing the CAME Domotic library. Happy automating! ## Usage examples This page walks you through the typical workflow for integrating with a CAME Domotic server: connecting, discovering what the server offers, fetching and controlling devices, and monitoring real-time changes. For a minimal “hello world” example, see [Getting started](#getting-started). **NOTE:** The examples below assume the library is installed (`pip install aiocamedomotic`). Unless otherwise noted, all code runs inside an `async with` block: ```python import asyncio from aiocamedomotic import CameDomoticAPI from aiocamedomotic.models import ( AnalogSensorType, DeviceType, DigitalInputStatus, LightStatus, LightType, LoadsCtrlProfile, OpeningStatus, ProfileDay, RelayStatus, ScenarioStatus, ServerFeature, ThermoProfile, ThermoZoneFanSpeed, ThermoZoneMode, ThermoZoneSeason, ThermoZoneStatus, Timer, TimerTimeSlot, TimerUpdate, DeviceUpdate, LightUpdate, OpeningUpdate, RelayUpdate, ThermoZoneUpdate, ScenarioUpdate, DigitalInputUpdate, AnalogInUpdate, EnergyMeterUpdate, LoadsCtrlMeterUpdate, LoadsCtrlRelayUpdate, PlantUpdate, WEEKDAYS, ) from aiocamedomotic.errors import ( CameDomoticError, CameDomoticServerNotFoundError, CameDomoticAuthError, CameDomoticServerTimeoutError, CameDomoticServerError, ) async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: ... ``` ### Connecting to the server #### Creating the API client Create a `CameDomoticAPI` instance with the async factory method. The `async with` statement ensures resources are cleaned up automatically: ```python import asyncio from aiocamedomotic import CameDomoticAPI async def main(): async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: server_info = await api.async_get_server_info() print(f"Connected to server: {server_info.keycode}") asyncio.run(main()) ``` **NOTE:** The session is **not** authenticated at creation time. The library authenticates lazily on the first real API call, like `async_get_server_info()`. If the credentials are invalid, a `CameDomoticAuthError` will be raised at that point. #### Using an existing HTTP session If you already have an `aiohttp.ClientSession` (e.g. in Home Assistant), pass it via the `websession` parameter: ```python async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password", websession=my_existing_session ) as api: ... ``` #### Handling connection errors The library raises specific exceptions for different failure scenarios: ```python from aiocamedomotic import CameDomoticAPI from aiocamedomotic.errors import ( CameDomoticServerNotFoundError, CameDomoticAuthError, CameDomoticServerTimeoutError, CameDomoticServerError, ) async def main(): try: async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: lights = await api.async_get_lights() except CameDomoticServerNotFoundError: print("Server not reachable. Check the IP address.") except CameDomoticAuthError: print("Authentication failed. Check your credentials.") except CameDomoticServerTimeoutError: print("Request timed out. The server may be busy, retry later.") except CameDomoticServerError as err: print(f"Server error: {err}") ``` The exception hierarchy is: - `CameDomoticError` — base class : - `CameDomoticServerNotFoundError` — host unreachable (transient) - `CameDomoticAuthError` — bad credentials or too many sessions - `CameDomoticServerError` — other server errors : - `CameDomoticServerTimeoutError` — request timeout (transient, retryable) ### Server configuration #### Server information Retrieve the server properties with `async_get_server_info()`. The `keycode` property serves as a unique identifier for the server: ```python server_info = await api.async_get_server_info() print(f"Keycode: {server_info.keycode}") print(f"Software version: {server_info.swver}") print(f"Server type: {server_info.type}") print(f"Board type: {server_info.board}") print(f"Serial number: {server_info.serial}") ``` Example output: ```text Keycode: 0000FFFF9999AAAA Software version: 1.2.3 Server type: 0 Board type: 3 Serial number: 0011ffee ``` #### Connectivity check Use `async_ping()` to verify the server is reachable and measure round-trip latency: ```python try: latency_ms = await api.async_ping() print(f"Server responded in {latency_ms:.1f} ms") except CameDomoticServerNotFoundError: print("Server is unreachable") except CameDomoticServerTimeoutError: print("Server timed out") ``` #### Available features The `features` property on `ServerInfo` lists the capabilities configured on the server. These are the functional blocks you would see in the official CAME Domotic mobile app (lights, openings, scenarios, etc.): ```python server_info = await api.async_get_server_info() for feature in server_info.features: print(f"Feature: {feature}") ``` Example output: ```text Feature: lights Feature: openings Feature: thermoregulation Feature: scenarios Feature: digitalin Feature: analogin Feature: energy Feature: loadsctrl ``` The `features` property returns plain strings whose known values are defined in [`ServerFeature`](#aiocamedomotic.models.ServerFeature). You can compare entries against enum members to decide which device APIs to call: ```python from aiocamedomotic.models import ServerFeature if ServerFeature.LIGHTS in server_info.features: lights = await api.async_get_lights() if ServerFeature.OPENINGS in server_info.features: openings = await api.async_get_openings() if ServerFeature.RELAYS in server_info.features: relays = await api.async_get_relays() if ServerFeature.THERMOREGULATION in server_info.features: zones = await api.async_get_thermo_zones() sensors = await api.async_get_analog_sensors() if ServerFeature.SCENARIOS in server_info.features: scenarios = await api.async_get_scenarios() if ServerFeature.DIGITALIN in server_info.features: digital_inputs = await api.async_get_digital_inputs() if ServerFeature.ANALOGIN in server_info.features: analog_inputs = await api.async_get_analog_inputs() if ServerFeature.TIMERS in server_info.features: timers = await api.async_get_timers() if ServerFeature.ENERGY in server_info.features: meters = await api.async_get_energy_meters() if ServerFeature.LOADSCTRL in server_info.features: controllers = await api.async_get_loadsctrl_meters() ``` #### Floors and rooms Retrieve the building topology to understand how devices are organized. `async_get_topology()` merges data from multiple server endpoints and nested device list commands, ensuring that floors and rooms are discovered even on servers where some endpoints return empty: ```python topology = await api.async_get_topology() for floor in topology.floors: print(f"Floor {floor.id}: {floor.name}") for room in floor.rooms: print(f" Room {room.id}: {room.name}") ``` Example output: ```text Floor 0: Ground Floor Room 1: Living Room Room 2: Kitchen Floor 1: First Floor Room 3: Bedroom Room 4: Bathroom ``` ### Working with devices All device types follow the same pattern: fetch the list, find a specific device, and control it. Lights are shown in full detail below; the other device types use the same approach with their own properties and methods. #### Lights **Fetching and inspecting lights:** ```python lights = await api.async_get_lights() for light in lights: print( f"ID: {light.act_id}, Name: {light.name}, " f"Status: {light.status}, Type: {light.type}" ) ``` Example output: ```text ID: 1, Name: Living Room Chandelier, Status: LightStatus.ON, Type: LightType.STEP_STEP ID: 2, Name: Hallway Night Light, Status: LightStatus.OFF, Type: LightType.DIMMER ID: 3, Name: RGB Strip, Status: LightStatus.ON, Type: LightType.RGB ``` **Finding a specific light:** ```python # By ID chandelier = next((l for l in lights if l.act_id == 1), None) # By name hallway = next((l for l in lights if l.name == "Hallway Night Light"), None) ``` **Controlling lights:** ```python from aiocamedomotic.models import LightStatus # Simple on/off (STEP_STEP lights) if chandelier: await chandelier.async_set_status(LightStatus.ON) await chandelier.async_set_status(LightStatus.OFF) # Dimmable lights: set brightness (0-100) if hallway: await hallway.async_set_status(LightStatus.ON, brightness=50) await hallway.async_set_status(LightStatus.ON, brightness=100) # RGB lights: set color as [R, G, B] (each 0-255) rgb_strip = next((l for l in lights if l.type == LightType.RGB), None) if rgb_strip: await rgb_strip.async_set_status(LightStatus.ON, rgb=[255, 0, 0]) await rgb_strip.async_set_status(LightStatus.ON, brightness=75, rgb=[0, 128, 255]) ``` **NOTE:** The `brightness` parameter is silently ignored for non-dimmable (STEP_STEP) lights. The `rgb` parameter is silently ignored for non-RGB lights. Dimmer hardware may quantize the requested brightness to its own steps, so the server can report back a slightly different value (e.g. requesting `50` may result in `52`). #### Openings Openings represent shutters, awnings, and similar motorized covers. They support opening, closing, stopping, and slat tilting (open/close) for covers with adjustable slats (e.g., venetian blinds). ```python import asyncio from aiocamedomotic.models import OpeningStatus openings = await api.async_get_openings() for opening in openings: print(f"ID: {opening.open_act_id}, Name: {opening.name}, Status: {opening.status}") # Control an opening shutter = next((o for o in openings if o.open_act_id == 10), None) if shutter: await shutter.async_set_status(OpeningStatus.OPENING) await asyncio.sleep(5) await shutter.async_set_status(OpeningStatus.STOPPED) await asyncio.sleep(5) await shutter.async_set_status(OpeningStatus.CLOSING) # Tilt slats (for covers with adjustable slats) await asyncio.sleep(5) await shutter.async_set_status(OpeningStatus.SLAT_OPEN) await asyncio.sleep(5) await shutter.async_set_status(OpeningStatus.SLAT_CLOSE) ``` #### Scenarios Scenarios are pre-configured automation sequences. They can only be activated (fire-and-forget); there is no bidirectional status control. ```python scenarios = await api.async_get_scenarios() for scenario in scenarios: print(f"ID: {scenario.id}, Name: {scenario.name}, Status: {scenario.scenario_status}") # Activate a scenario good_morning = next((s for s in scenarios if s.name == "Good morning"), None) if good_morning: await good_morning.async_activate() ``` **Recording a new custom scenario:** Custom (user-defined) scenarios are created by *recording* them: you put the server in recording mode, perform the actions you want the scenario to replay (e.g. switching lights on/off), and then finalize the recording. This mirrors the recording feature of the official CAME app. ```python # 1. Start recording a new scenario await api.async_start_scenario_recording("Movie night") # 2. Perform the actions to be captured, e.g. by pressing physical # switches on the plant, or via API commands: lights = await api.async_get_lights() living_room = next(l for l in lights if l.name == "Living room") await living_room.async_set_status(LightStatus.OFF) # 3. Finalize the recording: the server saves the new scenario and # the method returns it as a Scenario object movie_night = await api.async_stop_scenario_recording() print(f"Created scenario '{movie_night.name}' (ID: {movie_night.id})") # The new scenario can now be activated like any other one await movie_night.async_activate() ``` **NOTE:** Recording has been verified against a real plant with actions performed via **physical switches**. Actions sent through the API while recording are expected to be captured as well — the official CAME app records its own commands this way — but this has not been verified yet. `async_stop_scenario_recording()` identifies the new scenario by the name passed to `async_start_scenario_recording()` and returns `None` if it cannot be found (e.g. when finalizing a recording started by another client). **Renaming and deleting custom scenarios:** User-defined scenarios (`user_defined == 1`) can be renamed and deleted. System-defined scenarios are not meant to be modified: the command is sent anyway, but a warning is logged and the server behaviour is unverified. ```python scenarios = await api.async_get_scenarios() movie_night = next( (s for s in scenarios if s.name == "Movie night" and s.user_defined), None, ) if movie_night: # Rename the scenario (the local object is updated too) await movie_night.async_rename("Cinema mode") # Delete the scenario (irreversible!) await movie_night.async_delete() ``` #### Thermoregulation zones ```python zones = await api.async_get_thermo_zones() for zone in zones: print( f"ID: {zone.act_id}, Name: {zone.name}, " f"Temperature: {zone.temperature}°C, " f"Setpoint: {zone.set_point}°C, " f"Mode: {zone.mode}, Season: {zone.season}" ) ``` Example output: ```text ID: 1, Name: Living Room, Temperature: 20.0°C, Setpoint: 21.5°C, Mode: ThermoZoneMode.AUTO, Season: ThermoZoneSeason.WINTER ID: 52, Name: Bedroom, Temperature: 19.5°C, Setpoint: 20.0°C, Mode: ThermoZoneMode.MANUAL, Season: ThermoZoneSeason.WINTER ``` **Controlling thermoregulation zones:** ```python from aiocamedomotic.models import ThermoZoneFanSpeed, ThermoZoneMode, ThermoZoneSeason # Set target temperature (keeps current mode). # Only effective when the zone is in MANUAL mode; in AUTO or other modes # the server silently discards the new setpoint without returning an error. zone = zones[0] await zone.async_set_temperature(22.0) # To guarantee a setpoint change regardless of the current mode, switch to # MANUAL and set the temperature in a single call: await zone.async_set_config(mode=ThermoZoneMode.MANUAL, set_point=22.0) # Change operating mode (keeps current temperature) await zone.async_set_mode(ThermoZoneMode.MANUAL) # Full configuration with fan speed await zone.async_set_config( mode=ThermoZoneMode.MANUAL, set_point=21.5, fan_speed=ThermoZoneFanSpeed.MEDIUM, ) # Set fan speed (keeps current mode and temperature) await zone.async_set_fan_speed(ThermoZoneFanSpeed.SLOW) # Change global season for all zones (plant-level command — season cannot # be changed per zone) await api.async_set_thermo_season(ThermoZoneSeason.WINTER) ``` **WARNING:** **Setting the season to** `PLANT_OFF` **forces all zones to** `OFF`. When the season is set to `PLANT_OFF`, the CAME server automatically switches every thermoregulation zone to `ThermoZoneMode.OFF`. Reverting the season back to `WINTER` or `SUMMER` does **not** restore the previous zone modes — each zone stays `OFF` until its mode is changed manually (e.g. via `async_set_mode()` or `async_set_config()`). If your application needs to restore zone operation after re-enabling a season, you must track each zone’s previous mode yourself and re-apply it after changing the season. **NOTE:** Temperature values are returned as floats in degrees Celsius. The `fan_speed` parameter in `async_set_config` is optional; when provided, the `extended_infos` flag is set automatically. Season can only be changed at the plant level via `async_set_thermo_season()`. ##### Weekly setpoint profile Each thermo zone carries a weekly schedule that selects, hour by hour, which of the **five setpoint levels** shown in the official CAME app is active. The `profile` property exposes it as a typed [`ThermoProfile`](#aiocamedomotic.models.ThermoProfile) object with **8 rows**: Monday through Sunday, plus the special **JOLLY** profile (the schedule used while the zone is in `ThermoZoneMode.JOLLY`) as the 8th row. Rows are addressed with [`ProfileDay`](#aiocamedomotic.models.ProfileDay), whose `MONDAY`..\`\`SUNDAY\`\` values (0-6) match `datetime.date.weekday()`, so `ProfileDay(some_date.weekday())` always picks the right row. **Reading the profile:** ```python from datetime import datetime, time from aiocamedomotic.models import ProfileDay zones = await api.async_get_thermo_zones() zone = next((z for z in zones if z.name == "Office"), None) profile = zone.profile # Level active on a given day at a given moment print(profile.level_at(ProfileDay.MONDAY, 8)) # int hour (0-23) print(profile.level_at(ProfileDay.MONDAY, time(6, 30))) # any time of day # Level active right now now = datetime.now() print(profile.level_at(ProfileDay(now.weekday()), now)) # The JOLLY row is addressed like a day print(profile.level_at(ProfileDay.JOLLY, 12)) ``` **Viewing a day as time spans:** `spans()` returns a day’s schedule as runs of consecutive equal levels — handy for displaying the schedule the way the official app draws it: ```python for span in profile.spans(ProfileDay.MONDAY): print(f"{span.start} - {span.end}: level {span.level}") ``` Example output: ```text 00:00:00 - 08:00:00: level 1 08:00:00 - 09:00:00: level 4 09:00:00 - 15:00:00: level 3 15:00:00 - 00:00:00: level 1 ``` Each span is a half-open `[start, end)` range; the last span’s `end` of `00:00` means “through midnight”. The `profile_data` property exposes the same schedule in raw wire format: 8 strings of 96 characters (one per quarter hour of day), each character a digit `1`-`5`. The official app edits profiles per hour, so the four quarters within an hour normally share the same level; the typed API speaks in hours only. **NOTE:** Thermo profiles are currently **read-only**: the command the official app uses to write them has not been mapped yet, so the library cannot send an edited thermo profile back to the server. The editing methods described in [Weekly threshold profile](#loadsctrl-weekly-profile) work on `ThermoProfile` objects too (with one extra option: pass `days=aiocamedomotic.models.WEEKDAYS` to target Monday..Sunday while leaving the JOLLY row untouched, since `days=None` targets all 8 rows), but the result can only be used locally. Zone objects returned by `async_get_thermo_zones()` always carry the profile; `ThermoZoneUpdate` push updates do **not** include it, so read profiles from the zone list, not from updates. #### Analog sensors Analog sensors provide top-level readings (temperature, humidity, pressure) from the thermoregulation system. Each sensor carries an `AnalogSensorType` that identifies the kind of measurement it represents. **Fetching and inspecting sensors:** ```python from aiocamedomotic.models import AnalogSensorType sensors = await api.async_get_analog_sensors() for sensor in sensors: print( f"Name: {sensor.name}, Type: {sensor.sensor_type}, " f"Value: {sensor.value}, Unit: {sensor.unit}" ) ``` Example output: ```text Name: Outdoor Temperature, Type: AnalogSensorType.TEMPERATURE, Value: 21.5, Unit: C Name: Indoor Humidity, Type: AnalogSensorType.HUMIDITY, Value: 55, Unit: % Name: Barometric Pressure, Type: AnalogSensorType.PRESSURE, Value: 1013, Unit: hPa ``` **Filtering by sensor type:** ```python # Get only temperature sensors temp_sensors = [ s for s in sensors if s.sensor_type == AnalogSensorType.TEMPERATURE ] for s in temp_sensors: print(f"{s.name}: {s.value}°{s.unit}") # Find a specific sensor by ID outdoor = next((s for s in sensors if s.act_id == 100), None) ``` #### Digital inputs (binary sensors) Digital inputs are read-only binary sensors such as physical buttons or contact sensors. They report their state (ACTIVE/IDLE) but cannot be controlled remotely. `ACTIVE` means the input is triggered (e.g. a button is being pressed); `IDLE` means the input is in its normal resting state. ```python from aiocamedomotic.models import DigitalInputStatus digital_inputs = await api.async_get_digital_inputs() for di in digital_inputs: print( f"ID: {di.act_id}, Name: {di.name}, " f"Status: {di.status}, Address: {di.addr}" ) ``` Example output: ```text ID: 0, Name: digitalin_PvGCT, Status: DigitalInputStatus.UNKNOWN, Address: 200 ID: 1, Name: digitalin_BuTbB, Status: DigitalInputStatus.IDLE, Address: 201 ``` **Finding a specific digital input:** ```python # By ID button = next((di for di in digital_inputs if di.act_id == 1), None) # By name sensor = next((di for di in digital_inputs if di.name == "Front door button"), None) ``` **NOTE:** Some digital inputs do not report a `status` until their first state change. In that case, `status` returns `DigitalInputStatus.UNKNOWN`. #### Analog inputs (standalone sensors) Analog inputs are read-only standalone sensors exposed via the `analogin` feature. They provide a numeric reading and a unit of measurement and cannot be controlled remotely. **NOTE:** These sensors are independent of the thermoregulation system’s [`AnalogSensor`](#aiocamedomotic.models.AnalogSensor). The same physical sensor may appear in both endpoints. ```python if ServerFeature.ANALOGIN in server_info.features: analog_inputs = await api.async_get_analog_inputs() for ai in analog_inputs: print( f"ID: {ai.act_id}, Name: {ai.name}, " f"Value: {ai.value}, Unit: {ai.unit}" ) ``` Example output: ```text ID: 89, Name: Hygrometer, Value: 47.0, Unit: % ID: 90, Name: Outdoor Thermometer, Value: 21.5, Unit: C ID: 91, Name: Barometer, Value: 1013.0, Unit: hPa ``` **Finding a specific analog input:** ```python # By ID thermo = next((ai for ai in analog_inputs if ai.act_id == 90), None) # By name hygro = next((ai for ai in analog_inputs if ai.name == "Hygrometer"), None) ``` #### Relays Relays are simple on/off switches that can be controlled remotely. **Fetching and inspecting relays:** ```python from aiocamedomotic.models import RelayStatus relays = await api.async_get_relays() for relay in relays: print(f"ID: {relay.act_id}, Name: {relay.name}, Status: {relay.status}") ``` Example output: ```text ID: 31, Name: Garden Pump, Status: RelayStatus.ON ID: 32, Name: Gate Motor, Status: RelayStatus.OFF ``` **Finding a specific relay:** ```python # By ID pump = next((r for r in relays if r.act_id == 31), None) # By name gate = next((r for r in relays if r.name == "Gate Motor"), None) ``` **Controlling relays:** ```python if pump: await pump.async_set_status(RelayStatus.ON) await asyncio.sleep(5) await pump.async_set_status(RelayStatus.OFF) ``` #### Timers Timers are scheduling entities that define time-based activation windows for associated devices. Each timer has an enabled/disabled state, a day-of-week schedule, and up to 4 time slots. Timers support remote control: you can enable/disable them, toggle individual days, and configure the timetable. **Fetching and inspecting timers:** ```python timers = await api.async_get_timers() for timer in timers: print( f"ID: {timer.id}, Name: {timer.name}, " f"Enabled: {timer.enabled}, " f"Days: {timer.active_days}" ) for slot in timer.timetable: print( f" Slot {slot.index}: " f"start={slot.start_hour:02d}:{slot.start_min:02d}:{slot.start_sec:02d}" ) ``` Example output: ```text ID: 163, Name: Test timer, Enabled: True, Days: ['Monday', 'Wednesday', 'Friday'] Slot 0: start=10:00:00 ID: 164, Name: Timer 2, Enabled: True, Days: ['Tuesday', 'Thursday', 'Sunday'] Slot 1: start=12:00:00 Slot 2: start=11:00:00 ``` **Finding a specific timer:** ```python # By ID my_timer = next((t for t in timers if t.id == 163), None) # By name irrigation = next((t for t in timers if t.name == "Irrigation"), None) ``` ##### Understanding the days bitmask The `days` property is a 7-bit integer bitmask where each bit represents a day of the week. Bit 0 is Monday, bit 6 is Sunday: ```text Bit: 6 5 4 3 2 1 0 Day: Sun Sat Fri Thu Wed Tue Mon ``` Common values: - `1` — Monday only - `15` — Monday through Thursday (1+2+4+8) - `31` — Monday through Friday (weekdays) - `96` — Saturday and Sunday (weekend) - `127` — every day The `active_days` property returns a human-readable list, and `is_active_on_day()` checks a specific day: ```python timer = timers[0] print(timer.days) # 21 print(timer.active_days) # ['Monday', 'Wednesday', 'Friday'] print(timer.is_active_on_day(0)) # True (Monday) print(timer.is_active_on_day(1)) # False (Tuesday) ``` ##### Understanding the timetable Each timer has up to **4 time slots** (indices 0–3). The `timetable` property returns a list of [`TimerTimeSlot`](#aiocamedomotic.models.TimerTimeSlot) objects for the slots that are currently configured. Empty slots are simply absent from the list. Each `TimerTimeSlot` exposes: - `index` — the slot position (0–3) - `start_hour`, `start_min`, `start_sec` — the activation start time - `stop_hour`, `stop_min`, `stop_sec` — the stop time (`None` on some firmware versions) - `active` — whether the slot is individually active (`None` on some firmware versions) ```python for slot in timer.timetable: start = f"{slot.start_hour:02d}:{slot.start_min:02d}:{slot.start_sec:02d}" if slot.stop_hour is not None: stop = f"{slot.stop_hour:02d}:{slot.stop_min:02d}:{slot.stop_sec:02d}" else: stop = "N/A" print(f" Slot {slot.index}: {start} → {stop}") ``` Example output: ```text Slot 0: 10:00:00 → 18:30:00 Slot 2: 22:00:00 → N/A ``` **NOTE:** The `stop` and `active` fields may not be present in the server response. The corresponding properties return `None` in that case. Your code should handle both cases. ##### Enabling and disabling timers Toggle a timer’s global enabled state: ```python timer = timers[0] # Disable the timer await timer.async_disable() print(timer.enabled) # False # Re-enable the timer await timer.async_enable() print(timer.enabled) # True ``` ##### Toggling days of the week Add or remove individual days from the timer’s schedule. The `day` parameter is a zero-based index: 0 = Monday, 6 = Sunday. ```python # Enable Sunday (day index 6) await timer.async_enable_day(6) print(timer.active_days) # [..., 'Sunday'] # Disable Friday (day index 4) await timer.async_disable_day(4) print(timer.is_active_on_day(4)) # False ``` ##### Setting the timetable Use `async_set_timetable()` to configure all 4 time slots at once. Pass a list of exactly 4 entries — each is either a `(hour, minute, second)` tuple for an active slot, or `None` for an empty slot: ```python # Set slot 0 to 06:30:00, slot 2 to 22:00:00, leave slots 1 and 3 empty await timer.async_set_timetable([ (6, 30, 0), # slot 0 None, # slot 1 (empty) (22, 0, 0), # slot 2 None, # slot 3 (empty) ]) # Verify for slot in timer.timetable: print(f" Slot {slot.index}: {slot.start_hour:02d}:{slot.start_min:02d}") # Clear all slots await timer.async_set_timetable([None, None, None, None]) ``` **IMPORTANT:** `async_set_timetable()` always sends **all 4 slots** to the server. To keep existing slots unchanged, read the current timetable first and merge your changes: ```python # Read current slots into a 4-element list current: list[tuple[int, int, int] | None] = [None, None, None, None] for slot in timer.timetable: current[slot.index] = (slot.start_hour, slot.start_min, slot.start_sec) # Modify only slot 3 current[3] = (14, 30, 0) # Send the merged timetable await timer.async_set_timetable(current) ``` ##### Complete timer example This example fetches timers, prints their configuration, toggles the enabled state, adds a day, sets a time slot, and reverts everything: ```python import asyncio from aiocamedomotic import CameDomoticAPI async def main(): async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: timers = await api.async_get_timers() if not timers: print("No timers found") return timer = timers[0] print(f"Timer: {timer.name} (ID: {timer.id})") print(f" Enabled: {timer.enabled}") print(f" Days: {timer.active_days}") print(f" Slots: {len(timer.timetable)}") # Save original state was_enabled = timer.enabled had_sunday = timer.is_active_on_day(6) # Toggle enabled if was_enabled: await timer.async_disable() else: await timer.async_enable() print(f" Enabled toggled to: {timer.enabled}") # Toggle Sunday if had_sunday: await timer.async_disable_day(6) else: await timer.async_enable_day(6) print(f" Days now: {timer.active_days}") # Add a time slot current: list[tuple[int, int, int] | None] = [None] * 4 for slot in timer.timetable: current[slot.index] = ( slot.start_hour, slot.start_min, slot.start_sec ) current[3] = (14, 30, 0) await timer.async_set_timetable(current) print(f" Slots after adding slot 3: {len(timer.timetable)}") # Revert everything current[3] = None await timer.async_set_timetable(current) if had_sunday: await timer.async_enable_day(6) else: await timer.async_disable_day(6) if was_enabled: await timer.async_enable() else: await timer.async_disable() print(" Reverted to original state") asyncio.run(main()) ``` #### Energy meters Energy meters are read-only sensors exposed via the `energy` feature. They report the instantaneous power measured on a line (e.g. the whole-home consumption) together with energy values. Unlike lights or openings, energy meters are **plant-level entities keyed by** `id`: they have no `act_id` and no floor/room placement. The number of meters and their names are entirely plant-specific — always discover them via the API. **Fetching and inspecting energy meters:** ```python meters = await api.async_get_energy_meters() for meter in meters: print(f"ID: {meter.id}, Name: {meter.name}, " f"Power: {meter.instant_power} {meter.unit}") ``` Example output: ```text ID: 4, Name: Consumed Energy, Power: 595 W ID: 3, Name: Line 1 + Line 2, Power: 595 W ``` **Finding a specific energy meter:** ```python # By ID main_meter = next((m for m in meters if m.id == 4), None) # By name consumption = next((m for m in meters if m.name == "Consumed Energy"), None) ``` **Reading the energy values:** Each meter also exposes the raw `last_24h_avg` and `last_month_avg` fields, expressed in `energy_unit` (typically `Wh`): ```python if main_meter: print(f"last_24h_avg: {main_meter.last_24h_avg} {main_meter.energy_unit}") print(f"last_month_avg: {main_meter.last_month_avg} {main_meter.energy_unit}") ``` Real-time power readings are delivered as push updates — see [Energy meter updates](#energy-meter-updates) in the monitoring section below. **Resetting the energy history:** The stored energy history can be cleared with a single plant-level command. The reset applies to **all** energy meters at once (it cannot target a single meter) and is **irreversible**; instantaneous power readings are not affected: ```python await api.async_reset_energy_counters() ``` #### Loads control The `loadsctrl` feature implements **load shedding**: a loads **controller** ([`LoadsCtrlMeter`](#aiocamedomotic.models.LoadsCtrlMeter)) watches an energy meter and, when consumption exceeds its `max_power` threshold (with a `hysteresis` band around it to avoid flapping), it *detaches* its **managed loads** ([`LoadsCtrlRelay`](#aiocamedomotic.models.LoadsCtrlRelay)) — typically high-consumption appliances — one at a time until consumption drops below the threshold again. The order in which loads are shed is defined by each load’s `priority` value: **the lower the priority value, the earlier the load is detached**. Keep this direction in mind throughout this section — your most expendable appliance should have the *lowest* priority value. **NOTE:** The number and names of controllers and loads are **entirely plant-specific**: another plant may have any number of loads (including zero) with arbitrary user-defined names. Never hard-code load names or counts — always discover them via the API, as shown below. **Fetching the loads controllers:** ```python controllers = await api.async_get_loadsctrl_meters() for ctrl in controllers: print( f"ID: {ctrl.id}, Name: {ctrl.name}, " f"Max power: {ctrl.max_power} W, " f"Hysteresis: {ctrl.hysteresis} W, " f"Current power: {ctrl.power} W, " f"Energy meter ID: {ctrl.meter_id}" ) ``` Example output: ```text ID: 196612, Name: Consumed Energy, Max power: 5000 W, Hysteresis: 400 W, Current power: 595 W, Energy meter ID: 4 ``` The controller’s `meter_id` is the `id` of the energy meter it watches (see the [Energy meters]() section above); the controller’s own `id` is a separate, opaque value. **Fetching the managed loads:** The examples below reuse the `controllers` list fetched above. Get the loads managed by a controller with `async_get_relays()`, and sort them by `priority` to display the detach order (first row = first load shed): ```python if not controllers: print("No loads controller on this plant") else: ctrl = controllers[0] relays = sorted(await ctrl.async_get_relays(), key=lambda r: r.priority) for relay in relays: print( f"Priority: {relay.priority}, Name: {relay.name}, " f"Enabled: {relay.enabled}, Detached: {relay.detached}, " f"Status: {relay.status}" ) ``` Example output: ```text Priority: 129, Name: Washing machine, Enabled: False, Detached: False, Status: LoadsCtrlRelayStatus.ON Priority: 130, Name: Dishwasher, Enabled: True, Detached: False, Status: LoadsCtrlRelayStatus.ON Priority: 131, Name: Air conditioner, Enabled: False, Detached: False, Status: LoadsCtrlRelayStatus.ON Priority: 132, Name: Tumble dryer, Enabled: False, Detached: False, Status: LoadsCtrlRelayStatus.OFF ``` `detached` tells you whether the controller has currently shed the load, and `status` is the read-only relay output state (there is no loadsctrl command to switch the relay itself). **Finding a specific load:** ```python # By ID washer = next((r for r in relays if r.id == 65600), None) # By name dryer = next((r for r in relays if r.name == "Tumble dryer"), None) ``` **Enabling or disabling a load:** A load participates in load shedding only while it is *enabled* — this is the per-appliance toggle you see in the official CAME app. Disabling a load means the controller will never detach it, regardless of priority: ```python if washer: await washer.async_set_enabled(True) # controller may shed it await washer.async_set_enabled(False) # controller leaves it alone ``` **Changing a single load’s priority:** Use `async_set_priority()` to move one load within the detach order. Remember the direction: a *lower* value means the load is shed *earlier*. The wire command always carries both fields, so the current `enabled` flag is re-sent automatically alongside the new priority: ```python if washer: await washer.async_set_priority(130) ``` **Reordering the whole detach order:** To rewrite the full shedding sequence in one call, build the desired order (first element = first load shed) and pass it to `async_set_detach_order()`. The method reuses the priority values already present on the plant, reassigning them in ascending order to your sequence: ```python # Current order: washer, dishwasher, air conditioner, dryer. # Make the dishwasher the first load to shed, then the washer: by_name = {r.name: r for r in relays} await ctrl.async_set_detach_order([ by_name["Dishwasher"], by_name["Washing machine"], by_name["Air conditioner"], by_name["Tumble dryer"], ]) ``` **NOTE:** `async_set_detach_order()` writes **only the relays whose priority actually changes** (the official app does the same), so a no-op reorder sends no set commands. It raises `ValueError` if the sequence is not a permutation of the controller’s relays (same IDs, no duplicates). The method is not atomic (one set command per changed relay), but it is **safe to retry**: if a call fails partway through and leaves two relays sharing a priority value, calling it again repairs the duplicates into a strictly increasing sequence and converges to the requested order. **Updating the controller configuration:** `async_set_config()` changes the overload threshold (`max_power`) and/or the `hysteresis`. Unspecified values are re-sent unchanged, since the wire command requires the full configuration on every write: ```python await ctrl.async_set_config(max_power=4500) await ctrl.async_set_config(max_power=5000, hysteresis=300) ``` Configuration writes and load changes are echoed back as push updates — see [Loads control updates](#loadsctrl-updates) in the monitoring section below. ##### Weekly threshold profile Besides the fixed `max_power` value, each controller carries a weekly schedule that selects, hour by hour, which of the **five threshold levels** shown in the official CAME app is active (each level is a fraction of `max_power`). The `profile` property exposes it as a typed [`LoadsCtrlProfile`](#aiocamedomotic.models.LoadsCtrlProfile) object with 7 rows (Monday through Sunday) of 24 hourly slots, each set to a level `1`-`5`. **Reading the profile:** ```python from aiocamedomotic.models import ProfileDay profile = ctrl.profile # Level active on Monday at 08:00 print(profile.level_at(ProfileDay.MONDAY, 8)) # A day's schedule as runs of consecutive equal levels for span in profile.spans(ProfileDay.MONDAY): print(f"{span.start} - {span.end}: level {span.level}") ``` Example output: ```text 4 00:00:00 - 00:00:00: level 4 ``` Here the whole day runs at level 4, so there is a single span covering the full day: spans are half-open `[start, end)` ranges, and an `end` of `00:00` means “through midnight”. (The `profile_data` property exposes the same schedule in raw wire format: 7 strings of 24 hourly digits.) **Editing the profile:** Profiles are **immutable value objects**: every editing method returns a *new* profile and leaves the original untouched, so nothing reaches the server until you explicitly write the result back with `async_set_config()`. Edits are hour-based (`start` inclusive, `end` exclusive), matching how the official app edits profiles: ```python from aiocamedomotic.models import LoadsCtrlProfile, ProfileDay # Level 2 on Monday from 08:00 to 12:00 edited = profile.with_level(2, days=ProfileDay.MONDAY, start=8, end=12) # Level 1 every day from 22:00 through midnight # (days omitted → all days; end omitted → end of day) edited = edited.with_level(1, start=22) # Copy Monday's whole schedule onto the weekend edited = edited.with_day_copied( ProfileDay.MONDAY, to=[ProfileDay.SATURDAY, ProfileDay.SUNDAY] ) # Or start from scratch: every hour of every day at level 5 flat = LoadsCtrlProfile.constant(5) ``` Spans that cross midnight must be split into two calls (e.g. 22:00-06:00 is `with_level(1, start=22)` on one day plus `with_level(1, end=6)` on the next). **Writing the profile back:** Pass the edited profile to `async_set_config()` — alone or together with `max_power`/`hysteresis`. As with the other configuration values, the accepted state is echoed back as a `loadsctrl_meter_ind` push update: ```python await ctrl.async_set_config(profile_data=edited) # Re-fetch to confirm the server state ctrl = (await api.async_get_loadsctrl_meters())[0] print(ctrl.profile == edited) # True ``` **NOTE:** `profile_data` accepts a `LoadsCtrlProfile` object only (typically obtained from `ctrl.profile` and edited), not raw digit strings — to send a raw server-captured value, parse it first with `LoadsCtrlProfile.from_wire(raw_list)`. Profiles compare by value, so two profiles with the same grid are equal regardless of how they were built. #### Map pages Map pages are the floor-plan views configured in the official CAME app: each page is a background image with positioned device elements (lights, openings, thermostats, scenarios, links to other pages) laid on top. Maps are **read-only** — they provide positional information for building a visual UI, while device control always goes through the standard device APIs shown in the previous sections. **Fetching and inspecting map pages:** ```python pages = await api.async_get_map_pages() for page in pages: print( f"Page {page.page_id}: {page.page_label} " f"(elements: {len(page.elements)}, background: {page.background})" ) ``` Example output: ```text Page 0: Home (elements: 3, background: maps/maps_home.png) Page 1: Ground Floor (elements: 12, background: maps/maps_ground floor.png) Page 2: First Floor (elements: 8, background: maps/maps_first floor.png) ``` Page `0` is the root/home page. The `background` property is the relative URL path of the background image on the CAME server (full URL: `http:///`); the path may contain spaces, so percent-encode it before making HTTP requests. Elements are raw dictionaries preserving the server response structure, with common keys such as `x`, `y`, `width`, `height`, `type`, and `label`; additional keys depend on the element type (e.g. `act_id` for devices, `page` for page links, `scenario_id` for scenarios). The `x`/`y` coordinates range from `0` to `page_scale` (typically `1024`): ```python ground_floor = next((p for p in pages if p.page_label == "Ground Floor"), None) if ground_floor: for element in ground_floor.elements: print( f" {element.get('type')} '{element.get('label')}' " f"at ({element.get('x')}, {element.get('y')})" ) ``` ### Monitoring real-time updates The CAME Domotic server supports **long polling** for real-time status updates. When you call `async_get_updates()`, the request **blocks on the server** until one or more device state changes are detected, then returns an [`UpdateList`](#aiocamedomotic.models.UpdateList) containing all pending updates. This is the recommended mechanism for monitoring devices in real time without repeatedly fetching full device lists. #### Basic polling loop A typical polling loop continuously calls `async_get_updates()` and processes each batch of updates as it arrives: ```python import asyncio from aiocamedomotic.errors import CameDomoticServerTimeoutError async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password" ) as api: while True: try: updates = await api.async_get_updates(timeout=120) except CameDomoticServerTimeoutError: # Long poll timed out with no updates; simply retry continue for update in updates.get_typed_updates(): print(f"[{update.device_type.name}] {update.name} (ID: {update.device_id})") # Brief pause to avoid tight looping on rapid server responses await asyncio.sleep(1) ``` Example output: ```text [LIGHT] Living Room Chandelier (ID: 1) [OPENING] Bedroom Shutter (ID: 10) [THERMOSTAT] Living Room (ID: 1) ``` **NOTE:** The `asyncio.sleep(1)` acts as a safety throttle in case the server returns immediately (e.g. errors or rapid update bursts). If you need to run the polling loop alongside other logic, wrap it in `asyncio.create_task()`. #### Configuring timeouts All API methods use a default timeout of **30 seconds**, configurable via the `command_timeout` parameter when creating the API instance: ```python async with await CameDomoticAPI.async_create( "192.168.x.x", "username", "password", command_timeout=15 ) as api: lights = await api.async_get_lights() # uses 15s timeout ``` `async_get_updates()` is the only method that accepts its own `timeout` parameter, allowing it to use a different timeout than the rest of the API. This is because `async_get_updates()` uses **long polling** — the server holds the connection open until updates are available, which can take much longer than a regular command round-trip. A longer timeout (e.g. 60–120 seconds) is **strongly recommended** to avoid premature disconnections: ```python # Instance-level timeout is 15s for regular commands, # but async_get_updates uses its own 120s timeout updates = await api.async_get_updates(timeout=120) ``` **NOTE:** If no `timeout` is passed to `async_get_updates()`, it falls back to the instance-level `command_timeout` (default: 30s). For most real-time monitoring use cases, a timeout of **60–120 seconds** is recommended. #### Typed updates and filtering The `UpdateList` supports iteration over raw dicts for backward compatibility. For **typed** update objects with convenient properties, use `get_typed_updates()` or filter by device type with `get_typed_by_device_type()`: ```python updates = await api.async_get_updates() # Filter by device type light_updates = updates.get_typed_by_device_type(DeviceType.LIGHT) for light in light_updates: print( f"Light '{light.name}': status={light.status}, " f"type={light.light_type}, brightness={light.perc}%" ) # Dispatch by update type using isinstance for update in updates.get_typed_updates(): if isinstance(update, LightUpdate): print(f"Light '{update.name}': {update.status.name}, brightness={update.perc}%") elif isinstance(update, OpeningUpdate): print(f"Opening '{update.name}': {update.status.name}") elif isinstance(update, ThermoZoneUpdate): print( f"Thermo '{update.name}': {update.temperature}°C, " f"setpoint={update.set_point}°C, mode={update.mode.name}" ) elif isinstance(update, ScenarioUpdate): print(f"Scenario '{update.name}': {update.scenario_status.name}") elif isinstance(update, DigitalInputUpdate): print(f"Input '{update.name}': status={update.status}, addr={update.addr}") elif isinstance(update, AnalogInUpdate): print(f"Analog input '{update.name}': {update.value} {update.unit}") elif isinstance(update, RelayUpdate): print(f"Relay '{update.name}': {update.status.name}") elif isinstance(update, TimerUpdate): print( f"Timer '{update.name}': enabled={update.enabled}, " f"days={update.days}, slots={len(update.timetable)}" ) elif isinstance(update, EnergyMeterUpdate): print(f"Meter '{update.name}': {update.instant_power} {update.unit}") elif isinstance(update, LoadsCtrlRelayUpdate): print( f"Load '{update.name}': enabled={update.enabled}, " f"priority={update.priority}, detached={update.detached}" ) elif isinstance(update, LoadsCtrlMeterUpdate): print( f"Loads controller '{update.name}': " f"max_power={update.max_power} W, hysteresis={update.hysteresis} W" ) elif isinstance(update, PlantUpdate): print("Plant configuration changed, re-fetching devices...") ``` #### Handling plant updates A `plant_update_ind` signals that the **device configuration on the server has changed** (e.g. devices were added, removed, or reconfigured). When this happens, all locally cached device lists must be discarded and re-fetched: ```python updates = await api.async_get_updates() if updates.has_plant_update: lights = await api.async_get_lights() openings = await api.async_get_openings() relays = await api.async_get_relays() scenarios = await api.async_get_scenarios() timers = await api.async_get_timers() thermo_zones = await api.async_get_thermo_zones() sensors = await api.async_get_analog_sensors() digital_inputs = await api.async_get_digital_inputs() ``` **NOTE:** Plant updates are relatively rare. They typically occur when an installer modifies the system configuration. Failing to handle them may result in stale device data or missing newly added devices. #### Timer status updates When a timer is modified (enabled/disabled, day toggled, timetable changed), the server sends a `timer_info_ind` status update containing the **full current state** of the affected timer. This happens regardless of whether the change was made through this library, the CAME app, or the physical panel. The update payload mirrors the timer list response — it includes `name`, `id`, `enabled`, `days`, `bars` (the number of timetable slots reported by the server), and the complete `timetable` array. The library parses this into a [`TimerUpdate`](#aiocamedomotic.models.TimerUpdate) object. **Applying timer updates to cached objects:** If you maintain a local cache of `Timer` objects, you can update them when a `timer_info_ind` arrives: ```python # Assume `timers_cache` is a dict mapping timer ID → Timer object timers_cache = {t.id: t for t in await api.async_get_timers()} while True: try: updates = await api.async_get_updates(timeout=120) except CameDomoticServerTimeoutError: continue for update in updates.get_typed_updates(): if isinstance(update, TimerUpdate): cached = timers_cache.get(update.device_id) if cached: # Replace the raw_data with the fresh state from the # server — this updates all properties automatically cached.raw_data.update(update.raw_data) print( f"Timer '{cached.name}' updated: " f"enabled={cached.enabled}, " f"days={cached.active_days}, " f"slots={len(cached.timetable)}" ) await asyncio.sleep(1) ``` **Sequence of updates during a typical control session:** When you run a series of timer commands, the server sends one `timer_info_ind` for each change. For example, disabling a timer, then enabling Sunday, then adding a time slot, produces three consecutive updates: ```text timer_info_ind: enabled=0, days=15, timetable=[{index: 0, start: 10:00:00}] timer_info_ind: enabled=1, days=79, timetable=[{index: 0, start: 10:00:00}] timer_info_ind: enabled=1, days=79, timetable=[{index: 0, ...}, {index: 3, start: 14:30:00}] ``` Each update is a **complete snapshot** of the timer’s state — not a delta. You can safely overwrite the cached timer data with the update payload without needing to merge changes. **NOTE:** The `timer_info_ind` indication name was confirmed from real server traffic (firmware 3.0.1). A legacy variant `timer_update_ind` is also handled for firmware compatibility. #### Energy meter updates When the power measured by an energy meter changes, the server pushes a `meter_instant_power_ind` indication — one per meter, each containing a **complete snapshot** of the meter state (the same fields as the meter list response), with the energy values refreshed in the same push. The library parses it into an [`EnergyMeterUpdate`](#aiocamedomotic.models.EnergyMeterUpdate) object whose `device_id` is the meter’s `id`. This is the recommended way to track power consumption in real time, instead of repeatedly calling `async_get_energy_meters()`: ```python while True: try: updates = await api.async_get_updates(timeout=120) except CameDomoticServerTimeoutError: continue for update in updates.get_typed_updates(): if isinstance(update, EnergyMeterUpdate): print( f"Meter '{update.name}' (ID: {update.device_id}): " f"{update.instant_power} {update.unit}, " f"last_24h_avg={update.last_24h_avg} {update.energy_unit}" ) await asyncio.sleep(1) ``` Example output: ```text Meter 'Line 1 + Line 2' (ID: 3): 636 W, last_24h_avg=7788947 Wh Meter 'Consumed Energy' (ID: 4): 636 W, last_24h_avg=5813290 Wh ``` #### Loads control updates The server pushes a `loadsctrl_relay_ind` after **every accepted** `loadsctrl_relay_set_req` (enable/disable or priority change), and a `loadsctrl_meter_ind` after a controller configuration write. Both carry a **complete snapshot** of the affected entity, parsed into [`LoadsCtrlRelayUpdate`](#aiocamedomotic.models.LoadsCtrlRelayUpdate) / [`LoadsCtrlMeterUpdate`](#aiocamedomotic.models.LoadsCtrlMeterUpdate); for both, `device_id` is the loadsctrl `id` (not the relay’s `act_id`). **NOTE:** **Your own writes are echoed back to you.** These pushes are sent to *all* clients — including the one that issued the set command. A consumer polling `async_get_updates()` must therefore expect to receive updates for changes it made itself, and treat the pushed snapshot as the authoritative state (which makes it safe to simply overwrite any cached data). ```python while True: try: updates = await api.async_get_updates(timeout=120) except CameDomoticServerTimeoutError: continue for update in updates.get_typed_updates(): if isinstance(update, LoadsCtrlRelayUpdate): print( f"Load '{update.name}' (ID: {update.device_id}): " f"enabled={update.enabled}, priority={update.priority}, " f"detached={update.detached}" ) elif isinstance(update, LoadsCtrlMeterUpdate): print( f"Controller '{update.name}' (ID: {update.device_id}): " f"max_power={update.max_power} W, " f"hysteresis={update.hysteresis} W, power={update.power} W" ) await asyncio.sleep(1) ``` Example output (after enabling a load and swapping two priorities): ```text Load 'Washing machine' (ID: 65600): enabled=True, priority=129, detached=False Load 'Dishwasher' (ID: 65601): enabled=True, priority=129, detached=False Load 'Washing machine' (ID: 65600): enabled=True, priority=130, detached=False ``` ### Managing users The library exposes the same user administration features available in the official CAME app: listing the users defined on the server, creating and deleting users, changing passwords, and switching the session to another user. #### Listing users ```python users = await api.async_get_users() for user in users: print(f"User: {user.name}") ``` Example output: ```text User: admin User: family User: guest ``` #### Terminal groups Terminal groups define the permission scope assigned to users at creation time. Fetch them with `async_get_terminal_groups()` **before** adding a user, to discover the group names available on your server: ```python groups = await api.async_get_terminal_groups() for group in groups: print(f"Group {group.id}: {group.name}") ``` Example output: ```text Group 1: ETI/Domo ``` #### Adding a user Create a new user with `async_add_user()`. The `group` parameter takes a group **name** (e.g. `"ETI/Domo"`) as returned by `async_get_terminal_groups()` — not its numeric ID. The special value `"*"` (the default) may be used when fine-grained group assignment is not required: ```python new_user = await api.async_add_user("family", "s3cret_pwd") # Or with an explicit permission group new_user = await api.async_add_user("guest", "s3cret_pwd", group="ETI/Domo") ``` #### Changing a password Change a user’s password with `async_change_password()` on the `User` object: ```python users = await api.async_get_users() guest = next((u for u in users if u.name == "guest"), None) if guest: await guest.async_change_password("old_pwd", "new_pwd") ``` **NOTE:** Changing the password does not invalidate existing active sessions for that user — they remain valid until they expire; the new password is required at the next login. If the changed user is the currently authenticated one, the stored credentials of the active session are updated automatically — no additional action is required. #### Deleting a user Delete a user from the server with `async_delete()`: ```python if guest: await guest.async_delete() ``` The currently authenticated user cannot delete itself: calling `async_delete()` on it raises a `ValueError`. #### Switching the session user Use `async_set_as_current_user()` to log out the current user and continue the session as another one: ```python users = await api.async_get_users() admin = next((u for u in users if u.name == "admin"), None) if admin: await admin.async_set_as_current_user("admin_pwd") ``` If the login with the new credentials fails, a `CameDomoticAuthError` is raised and the previous credentials are restored, so the API client remains connected as the original user. ### Advanced topics #### Checking authentication status Session management is automatic and transparent. If you need to inspect the session status for any reason, use `is_session_valid()` on the `auth` attribute: ```python if api.auth.is_session_valid(): print("Session is authenticated and valid.") else: print("No valid session — it will be renewed automatically on the next call.") ``` **NOTE:** You rarely need to call this. The library handles reauthentication automatically whenever the session expires. #### Device autodiscovery The library exposes known MAC address OUI prefixes for CAME Domotic devices via `CAME_MAC_PREFIXES`. Combined with `async_is_came_endpoint()`, this allows identifying CAME ETI/Domo servers on the local network without credentials: ```python from aiocamedomotic import CAME_MAC_PREFIXES, async_is_came_endpoint async def async_discover_came_server(host: str, mac_address: str) -> bool: """Check if a network device is a CAME Domotic server. Args: host: IP address or hostname of the device. mac_address: MAC address in colon-separated uppercase hex format, e.g. "00:1C:B2:AA:BB:CC". """ # Step 1: Quick MAC prefix check (prefixes use "AA:BB:CC" format) mac_upper = mac_address.upper() if not any(mac_upper.startswith(prefix) for prefix in CAME_MAC_PREFIXES): return False # Step 2: Verify the device exposes a valid CAME API endpoint return await async_is_came_endpoint(host) ``` **NOTE:** `CAME_MAC_PREFIXES` contains prefixes in **colon-separated uppercase hex** format (e.g. `"00:1C:B2"`). Make sure the MAC address you pass uses the same format (`"00:1C:B2:AA:BB:CC"`) before comparing. Other common representations such as `001cb2aabbcc`, `00-1C-B2-AA-BB-CC`, or lowercase `00:1c:b2:aa:bb:cc` will **not** match directly — normalize to uppercase colon-separated format first, as shown in the example above. #### Debugging with traffic logging The library includes a built-in traffic logger that records every HTTP request and response exchanged with the CAME server. **Sensitive data (passwords, session tokens, server identifiers) is automatically anonymized**, so the output is safe to share publicly — for example, when reporting issues on GitHub. The traffic logger uses the `aiocamedomotic.traffic` logger name (a child of the main `aiocamedomotic` logger). Since it is a child logger, it inherits the handler and formatter already configured by the library — you only need to lower its level to `DEBUG`: ```python import logging # Enable traffic logging (anonymized request/response payloads) logging.getLogger("aiocamedomotic.traffic").setLevel(logging.DEBUG) ``` That single line is all you need. The traffic log messages will appear alongside the normal library output, using the same format. Example output: ```text 2025-03-15 10:23:01.123 DEBUG (MainThread) [aiocamedomotic.traffic] HTTP POST http://192.168.1.***/domo/ [status=200, 42.5ms] --> {"sl_cmd":"sl_registration_req","sl_login":"ad***","sl_pwd":"***"} <-- {"sl_client_id":"504***","sl_keep_alive_timeout_sec":900,"sl_data_ack_reason":0} ``` The following fields are redacted automatically: - `sl_pwd`, `sl_new_pwd` → fully replaced with `***` - `sl_login` → first 2 characters preserved (e.g. `ad***`) - `sl_client_id`, `client` → first 3 characters preserved (e.g. `504***`) - `keycode` → first 8 characters preserved (e.g. `61305E97********`) - `serial` → first 3 characters preserved (e.g. `037***`) - Camera URIs (`uri`, `uri_still`) → embedded credentials redacted - Host IP in the URL → last octet masked (e.g. `192.168.1.***`) - Usernames inside `sl_users_list` items → partially masked **NOTE:** The traffic logger level is independent from the main library logger. Setting `aiocamedomotic` to `WARNING` and `aiocamedomotic.traffic` to `DEBUG` is a valid configuration — you will see only the HTTP traffic, without the library’s internal debug messages. **TIP:** To write the traffic log to a **file** for later analysis, add a dedicated `FileHandler`. Use `propagate = False` if you want the traffic to go *only* to the file and not to the console: ```python import logging traffic_logger = logging.getLogger("aiocamedomotic.traffic") traffic_logger.setLevel(logging.DEBUG) traffic_logger.addHandler(logging.FileHandler("came_traffic.log")) traffic_logger.propagate = False # file only, no console output ``` **SEE ALSO:** For full API details, see the [API Reference](#api-reference). ## API Reference ### CAME Domotic API This module exposes the CAME Domotic API to the end-users. #### *class* aiocamedomotic.came_domotic_api.CameDomoticAPI(auth: [Auth](#aiocamedomotic.auth.Auth), \*, command_timeout: int = 30) Main class, exposes all the public methods of the CAME Domotic API. ##### *async* async_activate_scenario_by_name(name: str) → None Activate a scenario by its name, without fetching the scenario list. This is the plant-level counterpart to [`async_activate()`](#aiocamedomotic.models.Scenario.async_activate): the server resolves the scenario by `name` itself, so there is no need to first download the scenarios via [`async_get_scenarios()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_get_scenarios) just to trigger one — which is precisely the value of this command. The match is performed **server-side on the exact name**. Case sensitivity has not been verified, so pass the exact name as returned by [`async_get_scenarios()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_get_scenarios) (the `name` property of each [`Scenario`](#aiocamedomotic.models.Scenario)). If no scenario matches, the server silently performs no activation. * **Parameters:** **name** (*str*) – The exact name of the scenario to activate. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_add_user(username: str, password: str, group: str = '\*') → [User](#aiocamedomotic.models.User) Add a new user to the CAME Domotic server. * **Parameters:** * **username** (*str*) – The login name for the new user. * **password** (*str*) – The initial password for the new user. * **group** (*str* *,* *optional*) – The **name** of the permission group to assign to the new user (e.g. `"ETI/Domo"`). Defaults to `"*"`. Use [`async_get_terminal_groups()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_get_terminal_groups) to retrieve the available group names from the server. * **Returns:** A `User` object representing the newly created user. * **Return type:** [User](#aiocamedomotic.models.User) * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async classmethod* async_create(host: str, username: str, password: str, \*, websession: ClientSession | None = None, close_websession_on_disposal: bool = False, command_timeout: int = 30) → [CameDomoticAPI](#aiocamedomotic.came_domotic_api.CameDomoticAPI) Create a CameDomoticAPI object. * **Parameters:** * **host** (*str*) – The host of the CAME Domotic server. * **username** (*str*) – The username to use for the API. * **password** (*str*) – The password to use for the API. * **websession** (*aiohttp.ClientSession* *,* *optional*) – The aiohttp session to use for the API. If not provided, a new aiohttp.ClientSession will be created. * **close_websession_on_disposal** (*bool* *,* *default False*) – Controls whether the aiohttp session is closed when this object is disposed. - `False` (default): the session is **preserved** on disposal. Use this when the caller owns the session and reuses it elsewhere — which is the typical case in Home Assistant and other frameworks that maintain a single long-lived `aiohttp.ClientSession` shared across multiple integrations. Closing it here would break every other component that relies on it. - `True`: the session is closed on disposal. Use this only when you explicitly want this object to take ownership of the provided session and close it when done. #### NOTE When no `websession` is provided, this argument is ignored: the internally created session is always closed on disposal. * **command_timeout** (*int* *,* *optional*) – the default timeout in seconds for all commands sent to the server (default: 30s). * **Returns:** The CameDomoticAPI object. * **Return type:** [CameDomoticAPI](#aiocamedomotic.came_domotic_api.CameDomoticAPI) * **Raises:** [**CameDomoticServerNotFoundError**](#aiocamedomotic.errors.CameDomoticServerNotFoundError) – if the host doesn’t respond to an HTTP request or doesn’t expose the CAME Domotic API endpoint. **NOTE:** The session is not logged in until the first request is made. ##### *async* async_dispose() → None Dispose the CameDomoticAPI object. ##### *async* async_get_analog_inputs() → list[[AnalogIn](#aiocamedomotic.models.AnalogIn)] Get the list of all standalone analog input sensors on the server. Analog inputs are read-only sensors (e.g. hygrometers, thermometers, barometers) exposed via the `analogin` feature. They are independent of the thermoregulation system’s analog sensors. * **Returns:** List of analog input sensors. * **Return type:** list[[AnalogIn](#aiocamedomotic.models.AnalogIn)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_analog_sensors() → list[[AnalogSensor](#aiocamedomotic.models.AnalogSensor)] Get analog sensor readings from the thermoregulation system. Retrieves top-level temperature, humidity, and pressure sensor readings from the thermoregulation list response. * **Returns:** List of analog sensors found in the response. * **Return type:** list[[AnalogSensor](#aiocamedomotic.models.AnalogSensor)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_cameras() → list[[Camera](#aiocamedomotic.models.Camera)] Get the list of all TVCC cameras defined on the server. Cameras are read-only entities providing access to IP camera stream URIs. They do not support control commands. * **Returns:** List of cameras. * **Return type:** list[[Camera](#aiocamedomotic.models.Camera)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_digital_inputs() → list[[DigitalInput](#aiocamedomotic.models.DigitalInput)] Get the list of all digital input devices defined on the server. Digital inputs are read-only binary sensors (e.g. physical buttons, contact sensors). They report their state but cannot be controlled. * **Returns:** List of digital inputs. * **Return type:** list[[DigitalInput](#aiocamedomotic.models.DigitalInput)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_energy_meters() → list[[EnergyMeter](#aiocamedomotic.models.EnergyMeter)] Get the list of all energy meters defined on the server. Energy meters are read-only, plant-level entities exposed via the `energy` feature. They report the instantaneous power measured on a line and energy values, and have no floor/room placement. * **Returns:** List of energy meters. Returns an empty list if none are defined or the server response lacks the `array` key. * **Return type:** list[[EnergyMeter](#aiocamedomotic.models.EnergyMeter)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_floors() → list[[Floor](#aiocamedomotic.models.Floor)] Get the list of all the floors defined on the server. * **Returns:** List of floors. * **Return type:** list[[Floor](#aiocamedomotic.models.Floor)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_irrigation_sectors() → list[[Irrigation](#aiocamedomotic.models.Irrigation)] Get the list of all irrigation sectors defined on the server. Irrigation sectors are schedulable watering zones. Each can be forced on/off and its weekly schedule enabled/disabled. **NOTE:** Irrigation support is **not verified against a live plant**. See [`Irrigation`](#aiocamedomotic.models.Irrigation) for details. * **Returns:** List of irrigation sectors. * **Return type:** list[[Irrigation](#aiocamedomotic.models.Irrigation)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_lights() → list[[Light](#aiocamedomotic.models.Light)] Get the list of all the light devices defined on the server. * **Returns:** List of lights. * **Return type:** list[[Light](#aiocamedomotic.models.Light)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_loadsctrl_meters() → list[[LoadsCtrlMeter](#aiocamedomotic.models.LoadsCtrlMeter)] Get the list of all loads controllers defined on the server. Loads controllers (`loadsctrl` feature) bind an energy meter to an overload threshold and manage the load-shedding of their associated loads. A plant may define any number of controllers (including zero). * **Returns:** List of loads controllers. Returns an empty list if none are defined or the server response lacks the `array` key. * **Return type:** list[[LoadsCtrlMeter](#aiocamedomotic.models.LoadsCtrlMeter)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_loadsctrl_relays(controller_id: int) → list[[LoadsCtrlRelay](#aiocamedomotic.models.LoadsCtrlRelay)] Get the loads managed by the given loads controller. Convenience passthrough for `loadsctrl_relay_list_req`; [`async_get_relays()`](#aiocamedomotic.models.LoadsCtrlMeter.async_get_relays) is the ergonomic path. * **Parameters:** **controller_id** – The loads-controller `id` (as returned by [`async_get_loadsctrl_meters()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_get_loadsctrl_meters)), **not** the energy meter’s `id`. * **Returns:** List of managed loads. Returns an empty list if none are defined or the server response lacks the `array` key. * **Return type:** list[[LoadsCtrlRelay](#aiocamedomotic.models.LoadsCtrlRelay)] * **Raises:** * **ValueError** – If `controller_id` is not an integer. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_map_pages() → list[[MapPage](#aiocamedomotic.models.MapPage)] Get the list of all map pages (floor plans) from the server. Maps provide a spatial view of the installation with positioned device elements overlaid on background images. Map data is read-only and cannot be modified through the API. * **Returns:** List of map pages, each containing positioned elements. Returns an empty list if no maps are defined. * **Return type:** list[[MapPage](#aiocamedomotic.models.MapPage)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_openings() → list[[Opening](#aiocamedomotic.models.Opening)] Get the list of all opening devices defined on the server. * **Returns:** List of openings. * **Return type:** list[[Opening](#aiocamedomotic.models.Opening)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_relays() → list[[Relay](#aiocamedomotic.models.Relay)] Get the list of all relay devices defined on the server. Relays are simple on/off switches that can be controlled remotely. * **Returns:** List of relays. * **Return type:** list[[Relay](#aiocamedomotic.models.Relay)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_rooms() → list[[Room](#aiocamedomotic.models.Room)] Get the list of all the rooms defined on the server. * **Returns:** List of rooms. * **Return type:** list[[Room](#aiocamedomotic.models.Room)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_scenarios() → list[[Scenario](#aiocamedomotic.models.Scenario)] Get the list of all scenarios defined on the server. * **Returns:** List of scenarios. * **Return type:** list[[Scenario](#aiocamedomotic.models.Scenario)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_server_datetime() → [ServerDateTime](#aiocamedomotic.models.ServerDateTime) Get the current date and time of the CAME Domotic server. Reads the server clock, returned both as a Unix epoch (UTC) and as a local wall-clock string, together with the server timezone and the current daylight-saving-time flag. Useful for diagnosing the timestamps carried by push updates. * **Returns:** The server date/time information. * **Return type:** [ServerDateTime](#aiocamedomotic.models.ServerDateTime) * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_server_info() → [ServerInfo](#aiocamedomotic.models.ServerInfo) Get the server information. Provides info about the server (keycode, software version, etc.) and the list of features supported by the CAME Domotic server. * **Returns:** Server information. * **Return type:** [ServerInfo](#aiocamedomotic.models.ServerInfo) * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_sound_zones() → list[[SoundZone](#aiocamedomotic.models.SoundZone)] Get the list of all sound zones defined on the server. Sound zones are audio output rooms. Each can be powered on/off, muted, adjusted in volume, and switched between the available input sources. **NOTE:** Sound zone support is **not verified against a live plant**. See [`SoundZone`](#aiocamedomotic.models.SoundZone) for details. * **Returns:** List of sound zones. * **Return type:** list[[SoundZone](#aiocamedomotic.models.SoundZone)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_terminal_groups() → list[[TerminalGroup](#aiocamedomotic.models.TerminalGroup)] Get the list of terminal groups defined on the server. Terminal groups define the permission scope assigned to users at creation time. Call this method before [`async_add_user()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_add_user) to discover the available group names on the server. The `group` parameter of [`async_add_user()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_add_user) accepts a group **name** (e.g. `"ETI/Domo"`), not its numeric ID. The special value `"*"` (the default) may be used when fine-grained group assignment is not required. * **Returns:** Available groups. Returns an empty list if none are defined or the server response lacks the `array` key. * **Return type:** list[[TerminalGroup](#aiocamedomotic.models.TerminalGroup)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_thermo_zones() → list[[ThermoZone](#aiocamedomotic.models.ThermoZone)] Get the list of all thermoregulation zones defined on the server. * **Returns:** List of thermoregulation zones. * **Return type:** list[[ThermoZone](#aiocamedomotic.models.ThermoZone)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_timers() → list[[Timer](#aiocamedomotic.models.Timer)] Get the list of all timers defined on the server. Timers are scheduling entities that define time-based activation windows. They support enabling/disabling, day toggling, and timetable configuration. * **Returns:** List of timers. * **Return type:** list[[Timer](#aiocamedomotic.models.Timer)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_topology() → [PlantTopology](#aiocamedomotic.models.PlantTopology) Get the complete plant topology (floors and rooms). Merges data from the standard `floor_list_req` / `room_list_req` endpoints with the nested device list commands (`nested_light_list_req`, `nested_openings_list_req`, `nested_thermo_list_req`) to build a comprehensive topology even on servers where the flat floor/room endpoints return empty. Only nested commands for features supported by the server are sent. * **Returns:** The merged plant topology. * **Return type:** [PlantTopology](#aiocamedomotic.models.PlantTopology) * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_updates(timeout: int | None = None) → [UpdateList](#aiocamedomotic.models.UpdateList) Get status updates from the server using long polling. This method performs a long-polling request: it blocks until the server sends one or more real-time status updates (e.g., a light turned on, a scenario activated), then returns them all at once. * **Parameters:** **timeout** (*int* *|* *None* *,* *optional*) – the timeout in seconds for the long-polling request. If None, uses the instance-level `command_timeout` (default: 30s). Since this method uses long polling, a longer timeout (e.g. 120s) is recommended to avoid premature disconnections. * **Returns:** List of status updates received from the server. * **Return type:** [UpdateList](#aiocamedomotic.models.UpdateList) * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_get_users() → list[[User](#aiocamedomotic.models.User)] Get the list of users defined on the server. * **Returns:** List of users. Returns an empty list if no users are defined or if the server response doesn’t contain the users list. * **Return type:** list[[User](#aiocamedomotic.models.User)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_ping() → float Ping the CAME Domotic server and measure round-trip latency. Sends a keep-alive request to verify connectivity. If the session has expired, it transparently re-authenticates first. * **Returns:** Round-trip latency in milliseconds. * **Return type:** float * **Raises:** * [**CameDomoticServerNotFoundError**](#aiocamedomotic.errors.CameDomoticServerNotFoundError) – If the server is unreachable. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If authentication fails. * [**CameDomoticServerTimeoutError**](#aiocamedomotic.errors.CameDomoticServerTimeoutError) – If the request times out. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_reset_energy_counters() → None Reset the stored energy measurement history on the server. This is a plant-level command that clears the stored energy consumption history of **all** energy meters at once (e.g. the values behind `last_24h_avg` and `last_month_avg`); it cannot target a single meter. Instantaneous power readings are not affected. **WARNING:** The reset is irreversible: the server discards the stored energy history and there is no way to restore it. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_relay_status_by_name(name: str, status: [RelayStatus](#aiocamedomotic.models.RelayStatus)) → None Set a relay’s status by its name, without fetching the relay list. This is the plant-level counterpart to [`async_set_status()`](#aiocamedomotic.models.Relay.async_set_status): the server resolves the relay by `name` itself, so there is no need to first download the relays via [`async_get_relays()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_get_relays). The match is performed **server-side on the exact name**; pass the exact name as returned by [`async_get_relays()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_get_relays) (the `name` property of each [`Relay`](#aiocamedomotic.models.Relay)). **NOTE:** The by-name variant has **not** been verified against a live plant (our relays have never been tested against a real server; see the ROADMAP). Behaviour may differ across firmware versions. * **Parameters:** * **name** (*str*) – The exact name of the relay to control. * **status** ([*RelayStatus*](#aiocamedomotic.models.RelayStatus)) – Desired relay status (ON or OFF). * **Raises:** * **ValueError** – If `status` is `RelayStatus.UNKNOWN`. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_thermo_season(season: [ThermoZoneSeason](#aiocamedomotic.models.ThermoZoneSeason)) → None Set the global thermoregulation season for all zones. This is a plant-level command that changes the season for the entire thermoregulation system. **WARNING:** Setting `season` to `PLANT_OFF` causes the CAME server to automatically switch **all** thermoregulation zones to `ThermoZoneMode.OFF`. Reverting the season back to `WINTER` or `SUMMER` does **not** restore the previous zone modes — each zone stays `OFF` until its mode is changed explicitly via [`async_set_mode()`](#aiocamedomotic.models.ThermoZone.async_set_mode) or [`async_set_config()`](#aiocamedomotic.models.ThermoZone.async_set_config). If your application needs to restore zone operation after re-enabling a season, you must track each zone’s previous mode yourself and re-apply it after changing the season. * **Parameters:** **season** – The season to set. Valid values are `WINTER`, `SUMMER`, and `PLANT_OFF`. * **Raises:** * **ValueError** – If `season` is `ThermoZoneSeason.UNKNOWN`. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_start_scenario_recording(name: str) → None Start recording a new custom (user-defined) scenario. Puts the CAME server in scenario-recording mode: the actions performed on the plant after this call (e.g. switching lights on/off) are captured as the steps of a new scenario named `name`. Call [`async_stop_scenario_recording()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_stop_scenario_recording) to finalize the recording and save the scenario on the server. **NOTE:** The recording verified against a real plant captures actions performed via **physical switches**. Actions sent through the API (e.g. [`async_set_status()`](#aiocamedomotic.models.Light.async_set_status)) are expected to be captured as well — the official CAME app records its own commands this way — but this has not been verified yet. * **Parameters:** **name** (*str*) – The name of the new scenario. * **Raises:** * **ValueError** – If `name` is not a non-empty string. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error or rejects the recording request. ##### *async* async_stop_scenario_recording() → [Scenario](#aiocamedomotic.models.Scenario) | None Stop the ongoing scenario recording and save the new scenario. Finalizes the recording started with [`async_start_scenario_recording()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_start_scenario_recording): the server stores the captured actions as a new user-defined scenario. * **Returns:** The newly created scenario, retrieved from the server by matching the name passed to [`async_start_scenario_recording()`](#aiocamedomotic.came_domotic_api.CameDomoticAPI.async_start_scenario_recording) (if several user-defined scenarios share that name, the one with the highest ID is returned). Returns `None` if the recording was not started via this API instance or if the new scenario cannot be identified. * **Return type:** [Scenario](#aiocamedomotic.models.Scenario) | None * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error or rejects the finalization request. ### Entity models This module defines the Python representation of each of the entity types used by the CAME Domotic API. #### *class* aiocamedomotic.models.AnalogIn(raw_data: dict[str, Any]) Standalone analog input sensor from the `analogin_list_resp` endpoint. Represents a read-only analog sensor (e.g. hygrometer, thermometer, barometer) exposed via the `analogin` feature. * **Parameters:** **raw_data** – Dictionary containing the sensor data from the API. * **Raises:** **ValueError** – If `name` or `act_id` keys are missing from the input data. ##### *property* act_id *: int* Unique actuator/sensor identifier. ##### *property* name *: str* Display name of the sensor. ##### *property* unit *: str* Unit of measurement (e.g. `'C'`, `'%'`, `'hPa'`). ##### *property* value *: float* Sensor reading in the unit reported by [`unit`](#aiocamedomotic.models.AnalogIn.unit). #### *class* aiocamedomotic.models.AnalogInUpdate(raw_data: dict[str, Any]) Typed update for an analog input sensor (`analogin_status_ind` / `analogin_update_ind`). ##### *property* act_id *: int* Analog input actuator ID. ##### *property* unit *: str* Unit of measurement. ##### *property* value *: float* Sensor reading, with temperature scaling applied for unit `'C'`. #### *class* aiocamedomotic.models.AnalogSensor(raw_data: dict[str, Any], sensor_type: [AnalogSensorType](#aiocamedomotic.models.AnalogSensorType) = AnalogSensorType.UNKNOWN) Analog sensor from the CAME Domotic thermoregulation system. Represents a top-level temperature, humidity, or pressure sensor reading from the thermoregulation list response. These sensors are separate from the thermoregulation zones. * **Parameters:** * **raw_data** – Dictionary containing the sensor data from the API. * **sensor_type** – The type of sensor (temperature, humidity, or pressure). Defaults to `AnalogSensorType.UNKNOWN` if not specified. **NOTE:** The `value` property returns the real sensor reading in the unit reported by `unit` (e.g., degrees C, %, hPa). * **Raises:** **ValueError** – If `name` or `act_id` keys are missing from the input data. ##### *property* act_id *: int* ID of the analog sensor. ##### *property* name *: str* Name of the analog sensor. ##### sensor_type *: [AnalogSensorType](#aiocamedomotic.models.AnalogSensorType)* *= 'unknown'* The type of sensor (temperature, humidity, or pressure). ##### *property* unit *: str* Unit of measurement (e.g., ‘C’, ‘%’, ‘hPa’). ##### *property* value *: float* Sensor reading in the unit reported by `unit`. #### *class* aiocamedomotic.models.AnalogSensorType(\*values) Type of an analog sensor. Allowed values are: - TEMPERATURE (“temperature”) - HUMIDITY (“humidity”) - PRESSURE (“pressure”) - UNKNOWN (“unknown”) #### *class* aiocamedomotic.models.CameEntity Base class for all the CAME entities. #### *class* aiocamedomotic.models.Camera(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) TVCC camera entity in the CameDomotic API. Cameras are read-only devices that provide streaming video and still-image URIs for IP cameras connected to the CAME Domotic system. They cannot be controlled remotely — this is purely a viewing/monitoring feature. **WARNING:** The `uri` and `uri_still` fields point directly to camera HTTP endpoints on the local network. These URIs may contain embedded authentication credentials (e.g. `http://user:pass@camera/stream`). Avoid logging or displaying these values without sanitisation. * **Raises:** **ValueError** – If `name` or `id` keys are missing from the input data or the auth argument is not an instance of the expected `Auth` class. ##### *property* id *: int* Unique camera identifier. Unlike other device models which use `act_id`, cameras use a plain `id` field as their primary key (JS field: `id`, `idProperty`). ##### *property* is_flash *: bool* Whether this camera uses a Flash (SWF) stream. Flash streams are obsolete — consumers should fall back to [`uri_still`](#aiocamedomotic.models.Camera.uri_still) for cameras where this is `True`. Derived from: `stream_type == "swf"` (same check as JS client, line 5493). ##### *property* name *: str* `name`). * **Type:** Camera display name (JS field ##### *property* stream_type *: str* Raw stream format string as returned by the server. The only known value with special semantics is `"swf"` (Flash). All other values are treated identically by the JS client (JS field: `stream_type`). ##### *property* uri *: str* Primary streaming video URI. Points directly to the camera’s stream endpoint on the LAN. The actual protocol/format is opaque — the JS client loads it in an iframe (JS field: `uri`). ##### *property* uri_still *: str* Snapshot/still-image URI. Returns a single JPEG frame from the camera. Useful for thumbnails or as a fallback when the primary stream format is not supported. Append `?t=` for cache busting on repeated fetches (JS field: `uri_still`). #### *class* aiocamedomotic.models.DeviceType(\*values) Device type IDs used by the CAME ETI/Domo system. Each device in the CAME Domotic system is associated with one of these type identifiers. Not all device types are currently supported by this library. Negative IDs are library-specific: they identify entity kinds that the CAME API does not assign a numeric type to. Values: : - LOADSCTRL_RELAY (-5) - LOADSCTRL_METER (-4) - ANALOG_INPUT (-3) - ENERGY_SENSOR (-2) - ANALOG_SENSOR (-1) - LIGHT (0) - OPENING (1) - THERMOSTAT (2) - PAGE (3) - SCENARIO (4) - CAMERA (5) - SECURITY_PANEL (6) - SECURITY_AREA (7) - SECURITY_SCENARIO (8) - SECURITY_INPUT (9) - SECURITY_OUTPUT (10) - GENERIC_RELAY (11) - GENERIC_TEXT (12) - SOUND_ZONE (13) - DIGITAL_INPUT (14) - TIMER (15) #### *class* aiocamedomotic.models.DeviceUpdate(raw_data: dict[str, Any]) Base class for a typed status update from the CAME API. Wraps the raw update dict and exposes common properties. Subclasses add device-specific accessors. * **Parameters:** **raw_data** – The original dict from the `status_update_resp` result array. ##### *property* cmd_name *: str* The indication `cmd_name` string from the raw update. ##### *property* device_id *: int | None* Primary device identifier (`act_id`, `open_act_id`, or `id`). Returns the first available identifier, or `None` if none is present. ##### *property* device_type *: [DeviceType](#aiocamedomotic.const.DeviceType) | None* The [`DeviceType`](#aiocamedomotic.const.DeviceType) for this update, or `None` if the `cmd_name` is not recognized. ##### *property* name *: str* Device name from the update. ##### *property* update_indicator *: [UpdateIndicator](#aiocamedomotic.const.UpdateIndicator) | None* The [`UpdateIndicator`](#aiocamedomotic.const.UpdateIndicator) for this update, or `None` if the `cmd_name` is not recognized. #### *class* aiocamedomotic.models.DigitalInput(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Digital input (binary sensor) entity in the CameDomotic API. Digital inputs report their binary state (ACTIVE/IDLE) and cannot be switched remotely. Inputs that raise a technical alarm or keep a signalling counter can, however, be acknowledged via [`async_ack()`](#aiocamedomotic.models.DigitalInput.async_ack). * **Raises:** **ValueError** – If `name` or `act_id` keys are missing from the input data or the auth argument is not an instance of the expected `Auth` class. ##### *property* act_id *: int* ID of the digital input. ##### *property* addr *: int* Address of the digital input. ##### *async* async_ack() → None Acknowledge the digital input. Some digital inputs raise a technical alarm or keep a signalling counter that stays latched until it is acknowledged. This command clears that pending signalling for the input; it has no effect on inputs that do not track one. The command is keyed on [`addr`](#aiocamedomotic.models.DigitalInput.addr) (not [`act_id`](#aiocamedomotic.models.DigitalInput.act_id)). The server replies with `digitalin_ack_resp` echoing the input record, which is validated. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* name *: str* Name of the digital input. ##### *property* status *: [DigitalInputStatus](#aiocamedomotic.models.DigitalInputStatus)* ACTIVE (0) or IDLE (1). `ACTIVE` means the input is triggered (e.g. a button is being pressed). `IDLE` means the input is in its normal resting state. Returns `DigitalInputStatus.UNKNOWN` when the status field is absent from the server response (some digital inputs do not report a status until their first state change). * **Type:** Status of the digital input ##### *property* type *: [DigitalInputType](#aiocamedomotic.models.DigitalInputType)* Type of the digital input. Returns `DigitalInputType.UNKNOWN` when the type value is not recognized. ##### *property* utc_time *: int* UTC timestamp of the last event (Unix epoch seconds). Returns `0` if the digital input has never been triggered. #### *class* aiocamedomotic.models.DigitalInputStatus(\*values) Status of a digital input. Allowed values are: : - ACTIVE (0): the input is active (e.g. a button is pressed) - IDLE (1): the input is idle (e.g. a button is not pressed) #### *class* aiocamedomotic.models.DigitalInputType(\*values) Type of a digital input. Allowed values are: : - STATUS (1): standard status input #### *class* aiocamedomotic.models.DigitalInputUpdate(raw_data: dict[str, Any]) Typed update for a digital input (`digitalin_status_ind` / `digitalin_update_ind`). ##### *property* act_id *: int* Digital input actuator ID. ##### *property* addr *: int* Digital input address. ##### *property* status *: int* Digital input status value. ##### *property* utc_time *: int* UTC timestamp of the digital input event. #### *class* aiocamedomotic.models.EnergyMeter(raw_data: dict[str, Any]) Read-only energy meter from the `meters_list_resp` endpoint. Represents an energy meter exposed via the `energy` feature. Energy meters are plant-level entities keyed by [`id`](#aiocamedomotic.models.EnergyMeter.id) (they have no `act_id`, floor, or room). They report the current power reading and energy values, and cannot be controlled. * **Parameters:** **raw_data** – Dictionary containing the meter data from the API. * **Raises:** **ValueError** – If `name` or `id` keys are missing from the input data, or if `id` is not an integer. ##### *property* energy_unit *: str* Unit of measurement (`'Wh'` observed) for the [`last_24h_avg`](#aiocamedomotic.models.EnergyMeter.last_24h_avg) and [`last_month_avg`](#aiocamedomotic.models.EnergyMeter.last_month_avg) values. ##### *property* id *: int* Unique meter identifier. This is also the matching key for `meter_instant_power_ind` push updates (energy meters have no `act_id`). ##### *property* instant_power *: int* Current power reading, in the unit reported by [`unit`](#aiocamedomotic.models.EnergyMeter.unit). The value is passed through exactly as reported by the server (no unit conversion is applied). ##### *property* last_24h_avg *: int* Raw `last_24h_avg` field from the server, in [`energy_unit`](#aiocamedomotic.models.EnergyMeter.energy_unit). The value is passed through exactly as reported by the server. ##### *property* last_month_avg *: int* Raw `last_month_avg` field from the server, in [`energy_unit`](#aiocamedomotic.models.EnergyMeter.energy_unit). The value is passed through exactly as reported by the server. ##### *property* meter_type *: [EnergyMeterType](#aiocamedomotic.models.EnergyMeterType)* Type of the meter (POWER is the only value observed so far). ##### *property* name *: str* Display name of the meter. ##### *property* produced *: int* Raw `produced` field from the server. `0` observed on consumption meters; semantics for production meters are unverified. ##### *property* unit *: str* Unit of measurement for [`instant_power`](#aiocamedomotic.models.EnergyMeter.instant_power) (`'W'` observed). #### *class* aiocamedomotic.models.EnergyMeterType(\*values) Type of an energy meter, as reported in the `meter_type` field. Allowed values are: : - POWER (1): Power meter (the only value observed on real servers). - UNKNOWN (-1): Returned when the server reports an unrecognised `meter_type` value. #### *class* aiocamedomotic.models.EnergyMeterUpdate(raw_data: dict[str, Any]) Typed update for an energy meter (`meter_instant_power_ind`). Pushed by the server when the power measured by a meter changes. The payload is a complete snapshot of the meter state (same shape as a `meters_list_resp` item), including refreshed energy values. ##### *property* device_id *: int | None* For energy meters the primary ID is `id`. ##### *property* energy_unit *: str* Unit of measurement (`'Wh'` observed) for the [`last_24h_avg`](#aiocamedomotic.models.EnergyMeterUpdate.last_24h_avg) and [`last_month_avg`](#aiocamedomotic.models.EnergyMeterUpdate.last_month_avg) values. ##### *property* id *: int* Meter identifier (energy meters have no `act_id`). ##### *property* instant_power *: int* Current power reading, in the unit reported by [`unit`](#aiocamedomotic.models.EnergyMeterUpdate.unit). The value is passed through exactly as reported by the server (no unit conversion is applied). ##### *property* last_24h_avg *: int* Raw `last_24h_avg` field, in [`energy_unit`](#aiocamedomotic.models.EnergyMeterUpdate.energy_unit). The value is passed through exactly as reported by the server. ##### *property* last_month_avg *: int* Raw `last_month_avg` field, in [`energy_unit`](#aiocamedomotic.models.EnergyMeterUpdate.energy_unit). The value is passed through exactly as reported by the server. ##### *property* meter_type *: [EnergyMeterType](#aiocamedomotic.models.EnergyMeterType)* Type of the meter (POWER is the only value observed so far). ##### *property* produced *: int* Raw `produced` field (`0` observed on consumption meters). ##### *property* unit *: str* Unit of measurement for [`instant_power`](#aiocamedomotic.models.EnergyMeterUpdate.instant_power) (`'W'` observed). #### *class* aiocamedomotic.models.Floor(raw_data: dict[str, Any]) Floor entity in the CAME Domotic API. Represents a floor in the building structure with its identifier and name. ##### *property* id *: int* ID of the floor. ##### *property* name *: str* Name of the floor. #### *class* aiocamedomotic.models.Irrigation(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Irrigation sector entity in the CameDomotic API. Represents a single schedulable watering zone. Sectors are keyed on their `id` (not `act_id`). They can be forced on/off via [`async_force()`](#aiocamedomotic.models.Irrigation.async_force) and their weekly schedule enabled/disabled via [`async_set_enabled()`](#aiocamedomotic.models.Irrigation.async_set_enabled). **NOTE:** Not verified against a live plant — see the module docstring. * **Raises:** **ValueError** – If the `id` key is missing from the input data, or the auth argument is not an instance of the expected `Auth` class. ##### *async* async_force() → None Toggle a forced watering cycle for this sector. The command is a **toggle**: sending it starts a forced cycle if the sector is idle, and stops the running cycle otherwise. Callers that want explicit on/off semantics should check [`is_running`](#aiocamedomotic.models.Irrigation.is_running) before issuing the command. The server replies with a generic acknowledgement (no dedicated response command), so only the standard ack is validated. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_enabled(enabled: bool) → None Enable or disable the sector’s weekly schedule. * **Parameters:** **enabled** – `True` to enable the schedule, `False` to disable it. The server replies with a generic acknowledgement (no dedicated response command), so only the standard ack is validated. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* days *: int* Bitmask of active days (bit 0 = Monday, …, bit 6 = Sunday). ##### *property* enabled *: bool* Whether the sector’s weekly schedule is enabled. ##### *property* end *: Any* Raw end-window value, or `None` if absent. The exact shape is firmware-dependent and passed through unchanged. ##### *property* forced *: bool* Whether the sector is currently in a forced watering cycle. ##### *property* id *: int* Unique irrigation sector identifier. ##### *property* is_running *: bool* Whether the sector is currently watering. `True` when the sector reports a non-zero `status` or is in a forced cycle. ##### *property* name *: str | None* Display name of the sector, or `None` if the server omits it. ##### *property* perc *: int* Water percentage configured for the sector (`0` when absent). ##### *property* sprinklers *: Any* Raw sprinkler configuration, or `None` if absent. The exact shape is firmware-dependent and passed through unchanged. ##### *property* start *: Any* Raw start-window value, or `None` if absent. The exact shape is firmware-dependent and passed through unchanged. ##### *property* status *: int* Raw running status reported by the server (`0` when absent). #### *class* aiocamedomotic.models.Light(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Light entity in the CameDomotic API. * **Raises:** **ValueError** – If name or act_id keys are missing from the input data or the auth argument is not an instance of the expected Auth class. ##### *property* act_id *: int* ID of the light. ##### *async* async_set_status(status: [LightStatus](#aiocamedomotic.models.LightStatus), brightness: int | None = None, rgb: list[int] | None = None) → None Control the light. * **Parameters:** * **status** ([*LightStatus*](#aiocamedomotic.models.LightStatus)) – Status of the light. * **brightness** (*Optional* *[**int* *]*) – Brightness percentage of the light (range 0-100). If the brightness is not provided, it will stay unchanged. This argument is ignored for STEP_STEP lights. * **rgb** (*Optional* *[**List* *[**int* *]* *]*) – RGB color values as [R, G, B], each in range 0-255. If not provided, the color stays unchanged. This argument is ignored for non-RGB lights. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* floor_ind *: int | None* Floor index of the light. ##### *property* name *: str* Name of the light. ##### *property* perc *: int* Brightness percentage of the light (range 0-100). Non dimmable lights will always return 100. ##### *property* rgb *: list[int] | None* RGB color values of the light as [R, G, B], each in range 0-255. Returns None for non-RGB lights. ##### *property* room_ind *: int | None* Room index of the light. ##### *property* status *: [LightStatus](#aiocamedomotic.models.LightStatus)* Status of the light. Allowed values are OFF (0), ON (1) and AUTO (4). ##### *property* type *: [LightType](#aiocamedomotic.models.LightType)* Light type. Allowed values are “STEP_STEP” (normal lights), “DIMMER” (dimmable lights), and “RGB” (color lights). * **Raises:** **ValueError** – If the light type is not recognized. #### *class* aiocamedomotic.models.LightStatus(\*values) Status of a light. Allowed values are: : - OFF (0) - ON (1) - AUTO (4) #### *class* aiocamedomotic.models.LightType(\*values) Type of a light. Allowed values are: : - STEP_STEP (normal lights) - DIMMER (dimmable lights) - RGB (color lights with brightness via HSV V channel) #### *class* aiocamedomotic.models.LightUpdate(raw_data: dict[str, Any]) Typed update for a light device (`light_switch_ind` / `light_update_ind`). ##### *property* act_id *: int* Light actuator ID. ##### *property* floor_ind *: int* Floor index. ##### *property* light_type *: [LightType](#aiocamedomotic.models.LightType)* Light type (STEP_STEP, DIMMER, RGB). ##### *property* perc *: int* Brightness percentage (0-100). Defaults to 100 for non-dimmable. ##### *property* rgb *: list[int] | None* RGB color values `[R, G, B]` (0-255 each), or `None`. ##### *property* room_ind *: int* Room index. ##### *property* status *: [LightStatus](#aiocamedomotic.models.LightStatus)* Light status (OFF, ON, AUTO). #### *class* aiocamedomotic.models.LoadsCtrlMeter(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) The loads controller bound to an energy meter (`loadsctrl` feature). Binds an energy meter ([`meter_id`](#aiocamedomotic.models.LoadsCtrlMeter.meter_id)) to an overload threshold ([`max_power`](#aiocamedomotic.models.LoadsCtrlMeter.max_power)), a hysteresis, and a weekly hourly threshold profile ([`profile_data`](#aiocamedomotic.models.LoadsCtrlMeter.profile_data)). When consumption exceeds the threshold, the controller detaches its managed loads (fetched via [`async_get_relays()`](#aiocamedomotic.models.LoadsCtrlMeter.async_get_relays)) in priority order — lower priority value first. A plant may define any number of controllers (including zero); code should never assume a single controller. * **Raises:** **ValueError** – If `name` or `id` keys are missing from the input data, if `id` is not an integer, or if the auth argument is not an instance of the expected `Auth` class. ##### *async* async_get_relays() → list[[LoadsCtrlRelay](#aiocamedomotic.models.LoadsCtrlRelay)] Get the loads managed by this controller. Sends `loadsctrl_relay_list_req` with this controller’s [`id`](#aiocamedomotic.models.LoadsCtrlMeter.id). Relays are returned in server order; sort by [`LoadsCtrlRelay.priority`](#aiocamedomotic.models.LoadsCtrlRelay.priority) (ascending) to get the detach-order view shown by the official app. * **Returns:** List of managed loads. Returns an empty list if none are defined or the server response lacks the `array` key. * **Return type:** list[[LoadsCtrlRelay](#aiocamedomotic.models.LoadsCtrlRelay)] * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_config(\*, max_power: int | None = None, hysteresis: int | None = None, profile_data: [LoadsCtrlProfile](#aiocamedomotic.models.LoadsCtrlProfile) | None = None) → None Update the controller configuration. Sends `loadsctrl_meter_set_req`. The wire command requires the full triple, so any unspecified argument is re-sent with its current value from `raw_data`. * **Parameters:** * **max_power** – New overload threshold in Watts (positive integer), or `None` to keep the current value. * **hysteresis** – New hysteresis in Watts (non-negative integer; `0` disables the hysteresis band), or `None` to keep the current value. * **profile_data** – New weekly hourly threshold profile as a [`LoadsCtrlProfile`](#aiocamedomotic.models.LoadsCtrlProfile) (typically obtained from [`profile`](#aiocamedomotic.models.LoadsCtrlMeter.profile) and edited), or `None` to keep the current value. * **Raises:** * **ValueError** – If `max_power` is not a positive integer, if `hysteresis` is not a non-negative integer, or if `profile_data` is not a `LoadsCtrlProfile`. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_detach_order(relays: Sequence[[LoadsCtrlRelay](#aiocamedomotic.models.LoadsCtrlRelay)]) → None Rewrite priorities so loads are shed in the given order. The first element of `relays` is shed first (**lower priority value = shed first**). The method fetches the controller’s current relay list, takes the existing priority values, sorts them ascending, and reassigns them to `relays` in sequence order — sending one `loadsctrl_relay_set_req` per relay whose priority actually changes (the official app also writes only changed relays). Reusing the existing value set keeps whatever absolute numbering convention the plant uses intact. Each write is issued through a freshly-fetched relay object (not the caller-supplied one), so it carries the `enabled` flag as of this call’s own relay-list fetch rather than whatever value the caller’s object happened to have cached — otherwise a stale caller-side `enabled` would be silently written back over a concurrent change made by another client. The caller-supplied objects are updated with the resulting state afterward, so they remain a valid source for further calls. This method is not atomic: it issues one set command per changed relay, sequentially. If a call fails partway through, the plant can be left with two relays sharing the same priority value (the new value is written to one relay before the old value is cleared from the other). This is safe to recover from by simply calling the method again: duplicate priority values found on the plant are repaired into a strictly increasing sequence (each duplicate is bumped just above the value before it) before being reassigned, so a replay of the same request converges to the requested order. The same repair also applies to plants whose priorities contain duplicates for any other reason, in which case some relays end up with priority values not previously used on the plant; a warning is logged when this happens. * **Parameters:** **relays** – The desired detach order. Must be a permutation of this controller’s relays (same IDs, no duplicates) — fetch them via [`async_get_relays()`](#aiocamedomotic.models.LoadsCtrlMeter.async_get_relays) first. * **Raises:** * **ValueError** – If `relays` is not a permutation of this controller’s relays (same IDs, no duplicates). * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* hysteresis *: int* Hysteresis around the overload threshold, in Watts. ##### *property* id *: int* Loads-controller identifier (opaque server value). Required by `loadsctrl_relay_list_req`; do not confuse it with the bound energy meter’s [`meter_id`](#aiocamedomotic.models.LoadsCtrlMeter.meter_id). ##### *property* max_power *: int* Overload threshold in Watts. ##### *property* meter_id *: int* The `id` of the associated energy meter (from the meters list). ##### *property* name *: str* Controller name (mirrors the bound energy meter’s name). ##### *property* power *: float* Current power reading in Watts (same as the meter’s power). The value is passed through exactly as reported by the server (no unit conversion is applied). ##### *property* profile *: [LoadsCtrlProfile](#aiocamedomotic.models.LoadsCtrlProfile)* Weekly hourly threshold profile (typed view). Parsed fresh from `raw_data` on every access (never cached), so it always reflects the latest known server state. Edit it with the [`LoadsCtrlProfile`](#aiocamedomotic.models.LoadsCtrlProfile) methods and write it back via [`async_set_config()`](#aiocamedomotic.models.LoadsCtrlMeter.async_set_config). * **Raises:** **ValueError** – If `raw_data` lacks a well-formed `profile_data` value. ##### *property* profile_data *: list[str]* Weekly hourly threshold profile (raw wire format, copied). Seven strings — one per weekday, Monday first — of 24 characters each (one per hour of day). Each character is a digit `1`-`5` selecting the power threshold active in that hour as a fraction of [`max_power`](#aiocamedomotic.models.LoadsCtrlMeter.max_power). For a typed view use [`profile`](#aiocamedomotic.models.LoadsCtrlMeter.profile). Returns a copy: mutating the returned list does not affect `raw_data`. #### *class* aiocamedomotic.models.LoadsCtrlMeterUpdate(raw_data: dict[str, Any]) Typed update for a loads controller (`loadsctrl_meter_ind`). Pushed by the server after an accepted `loadsctrl_meter_set_req`, to **all** clients — including the one that issued the set. The payload is a complete snapshot of the controller state (same shape as a `loadsctrl_meter_list_resp` item). ##### *property* device_id *: int | None* For loads controllers the primary ID is `id`. ##### *property* hysteresis *: int* Hysteresis around the overload threshold, in Watts. ##### *property* id *: int* Loads-controller identifier (opaque server value). ##### *property* max_power *: int* Overload threshold in Watts. ##### *property* meter_id *: int* The `id` of the associated energy meter. ##### *property* power *: float* Current power reading in Watts (no unit conversion applied). ##### *property* profile_data *: list[str]* Weekly hourly threshold profile (raw wire format, copied). #### *class* aiocamedomotic.models.LoadsCtrlProfile(rows: Sequence[Sequence[int]]) Weekly hourly threshold profile of a loads controller. 7 rows (Monday..Sunday) of 24 hourly slots; each level `1`-`5` selects the power threshold active in that hour as a fraction of the controller’s `max_power` (the five levels shown in the official app). Used by [`LoadsCtrlMeter.profile`](#aiocamedomotic.models.LoadsCtrlMeter.profile) and accepted by [`LoadsCtrlMeter.async_set_config()`](#aiocamedomotic.models.LoadsCtrlMeter.async_set_config). ##### DAYS *: ClassVar[tuple[[ProfileDay](#aiocamedomotic.models.ProfileDay), ...]]* *= (ProfileDay.MONDAY, ProfileDay.TUESDAY, ProfileDay.WEDNESDAY, ProfileDay.THURSDAY, ProfileDay.FRIDAY, ProfileDay.SATURDAY, ProfileDay.SUNDAY)* The rows of the grid, in wire order. ##### WIRE_SLOTS_PER_DAY *: ClassVar[int]* *= 24* Number of wire slots per day row (a multiple of 24). #### *class* aiocamedomotic.models.LoadsCtrlRelay(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) A load managed by a loads controller (`loadsctrl` feature). Represents an appliance that the loads controller can *detach* (shed) when consumption exceeds the configured threshold. Loads are shed in priority order: **lower priority value = detached first**. Writable properties are changed via [`async_set_enabled()`](#aiocamedomotic.models.LoadsCtrlRelay.async_set_enabled) and [`async_set_priority()`](#aiocamedomotic.models.LoadsCtrlRelay.async_set_priority). * **Raises:** **ValueError** – If `name` or `id` keys are missing from the input data, if `id` is not an integer, or if the auth argument is not an instance of the expected `Auth` class. ##### *property* act_id *: int* Underlying actuator ID. Not used by any loadsctrl command: all loadsctrl operations are keyed by [`id`](#aiocamedomotic.models.LoadsCtrlRelay.id). ##### *async* async_set_enabled(enabled: bool) → None Enable or disable this load’s participation in load shedding. This is the equivalent of the per-appliance toggle in the official app. The wire command requires both fields, so the **current** [`priority`](#aiocamedomotic.models.LoadsCtrlRelay.priority) is re-sent alongside the new `enabled` flag. * **Parameters:** **enabled** – `True` to let the controller shed this load on overload, `False` to exclude it from load shedding. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_priority(priority: int) → None Set the detach-order priority (raw server value). **Lower priority value = detached first.** The wire command requires both fields, so the **current** [`enabled`](#aiocamedomotic.models.LoadsCtrlRelay.enabled) flag is re-sent alongside the new priority. * **Parameters:** **priority** – The new priority value (non-negative integer). The absolute numbering convention is plant-specific; the usual pattern is to swap/reuse the values already present on the plant (see [`LoadsCtrlMeter.async_set_detach_order()`](#aiocamedomotic.models.LoadsCtrlMeter.async_set_detach_order)). * **Raises:** * **ValueError** – If `priority` is not a non-negative integer. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* detached *: bool* Whether the controller has currently shed this load. ##### *property* enabled *: bool* Whether the load participates in load shedding. This is the per-appliance on/off toggle shown in the official app. ##### *property* id *: int* Load identifier. This is the key used by `loadsctrl_relay_set_req` and carried by `loadsctrl_relay_ind` push updates (not [`act_id`](#aiocamedomotic.models.LoadsCtrlRelay.act_id)). ##### *property* loadtype *: int* Raw `loadtype` field (opaque; `1` observed, semantics unknown). ##### *property* name *: str* Appliance name (user-defined, plant-specific). ##### *property* priority *: int* Detach-order priority (raw server value). **Lower value = detached first.** The absolute numbering is a plant-specific convention. ##### *property* status *: [LoadsCtrlRelayStatus](#aiocamedomotic.models.LoadsCtrlRelayStatus)* Current relay output state (read-only). #### *class* aiocamedomotic.models.LoadsCtrlRelayStatus(\*values) Current output state of a load-control relay. Allowed values are: : - OFF (0) - ON (1) - UNKNOWN (-1): Returned when the server reports an unrecognised status value. The relay output state is read-only: the loadsctrl commands cannot switch the relay on or off directly. #### *class* aiocamedomotic.models.LoadsCtrlRelayUpdate(raw_data: dict[str, Any]) Typed update for a load-control relay (`loadsctrl_relay_ind`). Pushed by the server after every accepted `loadsctrl_relay_set_req`, to **all** clients — including the one that issued the set. The payload is a complete snapshot of the relay state (same shape as a `loadsctrl_relay_list_resp` item) and should be treated as the authoritative confirmation of the change. ##### *property* act_id *: int* Underlying actuator ID (not used by loadsctrl commands). ##### *property* detached *: bool* Whether the controller has currently shed this load. ##### *property* device_id *: int | None* For loadsctrl relays the primary ID is `id`. Both `act_id` and `id` are present in the payload and differ: `id` is the key every loadsctrl command uses. ##### *property* enabled *: bool* Whether the load participates in load shedding. ##### *property* id *: int* Load identifier (the key used by all loadsctrl commands). ##### *property* loadtype *: int* Raw `loadtype` field (opaque; `1` observed). ##### *property* priority *: int* Detach-order priority (lower value = detached first). ##### *property* status *: [LoadsCtrlRelayStatus](#aiocamedomotic.models.LoadsCtrlRelayStatus)* Current relay output state (OFF, ON). #### *class* aiocamedomotic.models.MapPage(raw_data: dict[str, Any]) A page (floor plan) in the CAME Domotic map system. Each page represents a spatial view containing positioned elements (lights, openings, thermostats, page links, scenarios, cameras) overlaid on a background image. Elements are returned as raw dictionaries preserving the server response structure. The `elements` list contains dictionaries with at least the following common keys: `x`, `y`, `width`, `height`, `type`, `label`, `aspect`, `icon_id`, `permission`, `read_only`, `address`. Additional keys depend on the element type (e.g. `act_id` for devices, `page` for page links, `scenario_id` for scenarios). * **Raises:** **ValueError** – If `page_id` or `page_label` keys are missing from the input data, or if `page_id` is not an integer. ##### *property* background *: str* Relative URL path to the background image on the CAME server. The URL may contain spaces (e.g. `"maps/maps_pianta piano terra.png"`). Consumers must percent-encode the path when making HTTP requests. The full URL is constructed as `http:///`. ##### *property* elements *: list[dict[str, Any]]* Interactive elements placed on this map page. Each element is a raw dictionary from the server response. Common keys include `x`, `y`, `width`, `height`, `type`, `label`, `aspect`, `icon_id`, `permission`, `read_only`, and `address`. Type-specific keys (`act_id`, `page`, `scenario_id`, `status`) are present only for applicable element types. ##### *property* page_id *: int* Unique page identifier. `0` is the root/home page. ##### *property* page_label *: str* Human-readable page title. ##### *property* page_scale *: int* Coordinate space size for element positioning. Element `x`/`y` values range from `0` to `page_scale`. Typically `1024`. #### *class* aiocamedomotic.models.Opening(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Opening entity in the CameDomotic API. * **Raises:** **ValueError** – If name or open_act_id keys are missing from the input data or the auth argument is not an instance of the expected Auth class. ##### *async* async_set_status(status: [OpeningStatus](#aiocamedomotic.models.OpeningStatus)) → None Control the opening (open, close, stop, slat open, slat close). * **Parameters:** **status** ([*OpeningStatus*](#aiocamedomotic.models.OpeningStatus)) – Status to set for the opening. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* close_act_id *: int* Actuator ID for closing action. ##### *property* floor_ind *: int | None* Floor index where the opening is located. ##### *property* name *: str* Name of the opening. ##### *property* open_act_id *: int* Actuator ID for opening action. ##### *property* partial_positions *: list[str]* List of configured partial opening positions, if any. ##### *property* room_ind *: int | None* Room index where the opening is located. ##### *property* status *: [OpeningStatus](#aiocamedomotic.models.OpeningStatus)* STOPPED (0), OPENING (1), CLOSING (2), SLAT_OPEN (3) and SLAT_CLOSE (4). * **Type:** Current status of the opening. Allowed values ##### *property* type *: [OpeningType](#aiocamedomotic.models.OpeningType)* Opening type. * **Raises:** **ValueError** – If the opening type is not recognized. #### *class* aiocamedomotic.models.OpeningStatus(\*values) Status of an opening. Allowed values are: : - STOPPED (0) - OPENING (1) - CLOSING (2) - SLAT_OPEN (3) - SLAT_CLOSE (4) #### *class* aiocamedomotic.models.OpeningType(\*values) Type of an opening. Allowed values are: : - SHUTTER (0) - AWNING (1) - VENETIAN_BLIND (2) - GATE (3) #### *class* aiocamedomotic.models.OpeningUpdate(raw_data: dict[str, Any]) Typed update for an opening device (`opening_move_ind` / `opening_update_ind`). ##### *property* close_act_id *: int* Actuator ID for the closing action. ##### *property* device_id *: int | None* For openings the primary ID is `open_act_id`. ##### *property* floor_ind *: int* Floor index. ##### *property* open_act_id *: int* Actuator ID for the opening action. ##### *property* room_ind *: int* Room index. ##### *property* status *: [OpeningStatus](#aiocamedomotic.models.OpeningStatus)* Opening status (STOPPED, OPENING, CLOSING, etc.). #### *class* aiocamedomotic.models.PlantTopology(floors: list[[TopologyFloor](#aiocamedomotic.models.TopologyFloor)]) Complete plant topology (floors and rooms). Built by merging data from the standard `floor_list_req` / `room_list_req` endpoints and the nested device list commands (`nested_light_list_req`, `nested_openings_list_req`, `nested_thermo_list_req`). ##### floors *: list[[TopologyFloor](#aiocamedomotic.models.TopologyFloor)]* All floors in the plant, each containing its rooms. #### *class* aiocamedomotic.models.PlantUpdate(raw_data: dict[str, Any]) Marker update for `plant_update_ind`. When this indication is received, all cached devices must be discarded and re-fetched from the server. ##### *property* is_plant_update *: bool* Always returns `True`. #### *class* aiocamedomotic.models.ProfileDay(\*values) A row of a weekly profile grid. `MONDAY`..\`\`SUNDAY\`\` are `0`..\`\`6\`\`, matching both the wire row order (Monday first) and `datetime.date.weekday()`, so `ProfileDay(some_date.weekday())` is always correct. `JOLLY` (`7`) is the special thermo profile used while a thermo zone is in JOLLY mode; it is the 8th wire row of thermo profiles and is not a valid day for loadsctrl profiles. #### *class* aiocamedomotic.models.ProfileSpan(start: time, end: time, level: int) A run of consecutive slots sharing the same level (read-only view). Produced by [`WeeklyProfile.spans()`](#aiocamedomotic.models.WeeklyProfile.spans). The range is half-open: `start` is inclusive, `end` is exclusive; an `end` of `time(0)` means “through midnight” (end of day). #### *class* aiocamedomotic.models.Relay(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Relay entity in the CameDomotic API. Represents a generic relay (simple on/off switch) in the CAME Domotic system. Provides properties to read relay attributes and a method to control relay state. * **Raises:** **ValueError** – If `name` or `act_id` keys are missing from the input data or the auth argument is not an instance of the expected `Auth` class. ##### *property* act_id *: int* ID of the relay. ##### *async* async_set_status(status: [RelayStatus](#aiocamedomotic.models.RelayStatus)) → None Control the relay. * **Parameters:** **status** ([*RelayStatus*](#aiocamedomotic.models.RelayStatus)) – Desired relay status (ON or OFF). * **Raises:** * **ValueError** – If `status` is `RelayStatus.UNKNOWN`. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_status_timed(interval: int) → None Activate the relay for a timed interval. The relay switches ON and the server automatically switches it back OFF once `interval` elapses. The subsequent OFF transition is reported asynchronously by the server via the usual status update, so this method does not update the local [`status`](#aiocamedomotic.models.Relay.status) (which momentarily becomes ON). * **Parameters:** **interval** (*int*) – Activation time, passed verbatim to the server. Must be greater than 0. #### WARNING The unit of `interval` is **not documented** by the CAME API nor by the reference implementations this method is based on, and has not been verified against a real server. Callers should confirm the behaviour on their own plant before relying on a precise duration. * **Raises:** * **ValueError** – If `interval` is not greater than 0. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* floor_ind *: int* Floor index of the relay. ##### *property* name *: str* Name of the relay. ##### *property* room_ind *: int* Room index of the relay. ##### *property* status *: [RelayStatus](#aiocamedomotic.models.RelayStatus)* Status of the relay. Allowed values are OFF (0) and ON (1). #### *class* aiocamedomotic.models.RelayStatus(\*values) Status of a relay. Allowed values are: : - OFF (0) - ON (1) - UNKNOWN (-1): Returned when the server reports an unrecognised status value. Not a valid target for `async_set_status`. #### *class* aiocamedomotic.models.RelayUpdate(raw_data: dict[str, Any]) Typed update for a relay device (`relay_status_ind` / `relay_update_ind`). ##### *property* act_id *: int* Relay actuator ID. ##### *property* floor_ind *: int* Floor index. ##### *property* room_ind *: int* Room index. ##### *property* status *: [RelayStatus](#aiocamedomotic.models.RelayStatus)* Relay status (OFF, ON). #### *class* aiocamedomotic.models.Room(raw_data: dict[str, Any]) Room entity in the CAME Domotic API. Represents a room in the building structure with its identifier, name, and the floor it belongs to. ##### *property* floor_id *: int* ID of the floor this room belongs to. ##### *property* id *: int* ID of the room. ##### *property* name *: str* Name of the room. #### *class* aiocamedomotic.models.Scenario(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Scenario entity in the CameDomotic API. Represents a pre-configured automation scenario that can be activated to control multiple devices at once. * **Raises:** **ValueError** – If name or id keys are missing from the input data or the auth argument is not an instance of the expected Auth class. ##### *async* async_activate() → None Activate the scenario. Sends the scenario activation command to the CAME Domotic server. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_delete() → None Delete the scenario from the CAME Domotic server. **WARNING:** The deletion is irreversible: the server discards the scenario and there is no way to restore it. **NOTE:** Deletion is meant for **user-defined** scenarios (see [`user_defined`](#aiocamedomotic.models.Scenario.user_defined)): the official CAME app does not allow deleting system-defined ones. The command is sent anyway, but the server behaviour on system-defined scenarios is unverified. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_rename(name: str) → None Rename the scenario. Sends the scenario rename command to the CAME Domotic server and updates the local `name` accordingly. **NOTE:** Renaming is meant for **user-defined** scenarios (see [`user_defined`](#aiocamedomotic.models.Scenario.user_defined)): the official CAME app does not allow renaming system-defined ones. The command is sent anyway, but the server behaviour on system-defined scenarios is unverified. * **Parameters:** **name** (*str*) – The new name of the scenario. * **Raises:** * **ValueError** – If `name` is not a non-empty string. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* icon_id *: int* Icon ID associated with the scenario. ##### *property* id *: int* ID of the scenario. ##### *property* name *: str* Name of the scenario. ##### *property* scenario_status *: [ScenarioStatus](#aiocamedomotic.models.ScenarioStatus)* OFF (0), TRIGGERED (1), or ACTIVE (2). * **Type:** Scenario-specific status ##### *property* status *: int* General status of the scenario. ##### *property* user_defined *: int* Whether the scenario is user-defined (1) or system-defined (0). #### *class* aiocamedomotic.models.ScenarioStatus(\*values) Status of a scenario. Allowed values are: : - OFF (0): scenario is not active - TRIGGERED (1): scenario has just been activated and is executing - ACTIVE (2): scenario is currently in effect #### *class* aiocamedomotic.models.ScenarioUpdate(raw_data: dict[str, Any]) Typed update for a scenario (`scenario_status_ind` / `scenario_activation_ind` / `scenario_user_ind`). ##### *property* device_id *: int | None* For scenarios the primary ID is `id`. ##### *property* id *: int* Scenario ID. ##### *property* scenario_status *: [ScenarioStatus](#aiocamedomotic.models.ScenarioStatus)* Scenario status (OFF, TRIGGERED, ACTIVE). #### *class* aiocamedomotic.models.ServerDateTime(raw_data: dict[str, Any]) Date and time reported by a CAME Domotic server. Returned by `CameDomoticAPI.async_get_server_datetime()`. Wraps the `datetime_req` response, which carries the server clock both as a Unix epoch (UTC) and as a local wall-clock string, plus the server timezone and the current daylight-saving-time flag. Useful for diagnosing the timestamps carried by push updates. ##### *property* datetime_string *: str | None* Local wall-clock time as a `"YYYY-MM-DD HH:MM:SS"` string. Already offset for the server timezone and DST. `None` if the server response does not include it. ##### *property* daylight_saving_time *: bool* Whether daylight saving time is currently in effect on the server. ##### *property* epoch *: int* Server clock as a Unix epoch, in seconds (UTC). ##### *property* timezone_name *: str | None* IANA timezone name of the server (e.g. `"Europe/Rome"`). `None` if the server response does not include it. ##### *property* utc_datetime *: datetime* Server clock as a timezone-aware `datetime` (UTC). Derived from [`epoch`](#aiocamedomotic.models.ServerDateTime.epoch). #### *class* aiocamedomotic.models.ServerFeature(\*values) Server feature identifiers reported by the CAME Domotic server. Each member corresponds to a functional block (e.g. lights, openings). Because `ServerFeature` is a `StrEnum`, each member **is** its string value (e.g. `ServerFeature.LIGHTS == "lights"` is `True`), so members can be compared directly against the plain strings in `features`. Values: : - `LIGHTS` (“lights”) — on/off, dimmable and RGB lights - `OPENINGS` (“openings”) — shutters, awnings, and motorized covers - `RELAYS` (“relays”) — simple on/off relay switches - `THERMOREGULATION` (“thermoregulation”) — climate zones and analog sensors - `SCENARIOS` (“scenarios”) — pre-configured automation sequences - `DIGITALIN` (“digitalin”) — read-only binary sensors (buttons, contacts) - `ANALOGIN` (“analogin”) — read-only standalone analog sensors - `ENERGY` (“energy”) — energy meters - `LOADSCTRL` (“loadsctrl”) — load control / management - `TIMERS` (“timers”) — time-based scheduling entities #### *class* aiocamedomotic.models.ServerInfo(keycode: str, serial: str, features: list[str], swver: str | None = None, type: str | None = None, board: str | None = None) Server information of a CAME Domotic server. ##### board *: str | None* *= None* Board type of the server. ##### features *: list[str]* List of feature strings reported by the server. Each feature corresponds to a functional block (e.g. lights, openings). Values are plain strings whose known values are defined in [`ServerFeature`](#aiocamedomotic.models.ServerFeature). Because `ServerFeature` is a `StrEnum`, you can compare entries against enum members directly (e.g. `ServerFeature.LIGHTS in server_info.features`). The return type is kept as `list[str]` so that features introduced by newer firmware versions are preserved even if the library does not yet define them in [`ServerFeature`](#aiocamedomotic.models.ServerFeature). ##### keycode *: str* Keycode of the server (CAME proprietary unique identifier). ##### serial *: str* Serial number of the server. ##### swver *: str | None* *= None* Software version of the server. ##### type *: str | None* *= None* Type of the server. #### *class* aiocamedomotic.models.SoundZone(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Sound zone entity in the CameDomotic API. Represents a single audio zone. Zones are keyed on their `id` (not `act_id`). They can be powered on/off, muted, adjusted in volume, and switched between the available input sources. **NOTE:** Not verified against a live plant — see the module docstring. * **Raises:** **ValueError** – If the `id` key is missing from the input data, or the auth argument is not an instance of the expected `Auth` class. ##### *async* async_refresh() → None Refresh this zone’s state from the server (`sound_room_src_req`). Fetches the current state of this single zone and merges it into `raw_data`. The update is applied only if the response refers to this zone’s ID. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_select_source(source_name: str) → None Select an input source by name. The name is matched against the normalized [`sources`](#aiocamedomotic.models.SoundZone.sources) list and the corresponding source ID is sent to the server. * **Parameters:** **source_name** – Name of the source to select, as reported in [`sources`](#aiocamedomotic.models.SoundZone.sources). * **Raises:** * **ValueError** – If no source with the given name (and a valid ID) is available on this zone. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_send_source_command(source_id: int, action: str) → None Send an advanced command to an audio source (`suftif_cmd_req`). This is a low-level primitive: the set of supported actions depends on the source device (e.g. tuner or media player transport commands) and is passed through to the server unchanged. * **Parameters:** * **source_id** – ID of the target source, as reported in [`sources`](#aiocamedomotic.models.SoundZone.sources). * **action** – Action string understood by the source device. The server replies with a generic acknowledgement (no dedicated response command), so only the standard ack is validated. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_mute(muted: bool) → None Mute or unmute the zone. * **Parameters:** **muted** – `True` to mute the zone, `False` to unmute it. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_volume_level(volume_level: float) → None Set the zone volume from a normalized 0.0–1.0 level. The level is denormalized onto the server-provided `min_volume`/`max_volume` range and rounded to the nearest raw value. * **Parameters:** **volume_level** – Desired volume in the 0.0–1.0 range. * **Raises:** * **ValueError** – If `volume_level` is outside the 0.0–1.0 range, or the zone does not report its volume range. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_turn_off() → None Put the zone in standby. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_turn_on() → None Power on the zone (leave standby). * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* id *: int* Unique sound zone identifier. ##### *property* is_muted *: bool* Whether the zone is muted. ##### *property* is_on *: bool* Whether the zone is powered on (i.e. not in standby). ##### *property* max_volume *: int | None* Upper bound of the raw volume range, or `None` if absent. ##### *property* min_volume *: int | None* Lower bound of the raw volume range, or `None` if absent. ##### *property* name *: str | None* Display name of the zone, or `None` if the server omits it. ##### *property* source *: str | None* Name of the currently selected source, or `None` if unknown. ##### *property* sources *: list[dict[str, Any]]* Available input sources, normalized to `{"name", "id"}` dicts. Depending on the firmware, the server reports sources either as a `sources` array (items carrying `source` or `source_name` plus `id`) or as flat `source_N` fields with an optional `id_source_N` companion (defaulting to `N - 1`). Both formats are merged and deduplicated by `(name, id)`. ##### *property* volume *: int | None* Raw volume value reported by the server, or `None` if absent. ##### *property* volume_level *: float | None* Volume normalized to the 0.0–1.0 range. Returns `None` when the raw volume or its range is unknown, and `0.0` when the server reports an empty range (`max_volume == min_volume`). Values are clamped to 0.0–1.0. #### *class* aiocamedomotic.models.SoundZoneAction(\*values) Actions accepted by the `sound_switch_req` command. Values: : - STANDBY (“standby”): power the zone on/off - MUTE (“mute”): mute/unmute the zone - VOLUME (“volume”): set the raw volume value - SOURCE (“source”): select an input source by ID #### *class* aiocamedomotic.models.TerminalGroup(raw_data: dict[str, Any]) Terminal group in the CAME Domotic API. Represents a user permission group (e.g. `"ETI/Domo"`). Groups are assigned to users at creation time via `CameDomoticAPI.async_add_user()`. Use `CameDomoticAPI.async_get_terminal_groups()` to retrieve the available group names before creating a user. ##### *property* id *: int* Numeric ID of the group. ##### *property* name *: str* Name of the group (e.g. `"ETI/Domo"`). #### *class* aiocamedomotic.models.ThermoProfile(rows: Sequence[Sequence[int]]) Weekly setpoint-level profile of a thermo zone. 8 rows — Monday..Sunday plus `ProfileDay.JOLLY` as the 8th row — of 96 quarter-hour wire slots; each level `1`-`5` selects one of the five setpoint levels shown in the official app. The app edits thermo profiles per hour, so this class exposes hours only (see [`WeeklyProfile`](#aiocamedomotic.models.WeeklyProfile)). Currently a read/edit value type only: the thermo profile set command has never been observed in captured traffic, so writing a profile back to a zone is not yet supported by the library. ##### DAYS *: ClassVar[tuple[[ProfileDay](#aiocamedomotic.models.ProfileDay), ...]]* *= (ProfileDay.MONDAY, ProfileDay.TUESDAY, ProfileDay.WEDNESDAY, ProfileDay.THURSDAY, ProfileDay.FRIDAY, ProfileDay.SATURDAY, ProfileDay.SUNDAY, ProfileDay.JOLLY)* The rows of the grid, in wire order. ##### WIRE_SLOTS_PER_DAY *: ClassVar[int]* *= 96* Number of wire slots per day row (a multiple of 24). #### *class* aiocamedomotic.models.ThermoZone(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Thermoregulation zone entity in the CameDomotic API. Represents a single thermoregulation zone with its current state, including temperature readings, setpoint, operating mode, and season. Temperature values from the API are integers multiplied by 10 (e.g., 215 = 21.5 degrees C). Properties in this class return converted float values in degrees. * **Raises:** **ValueError** – If `name` or `act_id` keys are missing from the input data, or the auth argument is not an instance of the expected `Auth` class. ##### *property* act_id *: int* ID of the thermoregulation zone. ##### *property* antifreeze *: float | None* Antifreeze temperature in degrees Celsius, or None if not set. ##### *async* async_set_config(mode: [ThermoZoneMode](#aiocamedomotic.models.ThermoZoneMode), set_point: float, \*, fan_speed: [ThermoZoneFanSpeed](#aiocamedomotic.models.ThermoZoneFanSpeed) | None = None) → None Configure the thermoregulation zone. **NOTE:** The season cannot be changed via this method. Use `CameDomoticAPI.async_set_thermo_season()` to change the season at the plant level. * **Parameters:** * **mode** – Operating mode to set. * **set_point** – Target temperature in degrees Celsius. * **fan_speed** – Fan speed setting (optional, requires extended info). * **Raises:** * **ValueError** – If `mode` or `fan_speed` is `UNKNOWN`. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_fan_speed(fan_speed: [ThermoZoneFanSpeed](#aiocamedomotic.models.ThermoZoneFanSpeed)) → None Set the fan speed, keeping the current mode and temperature. * **Parameters:** **fan_speed** – Fan speed to set. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_mode(mode: [ThermoZoneMode](#aiocamedomotic.models.ThermoZoneMode)) → None Set the operating mode, keeping the current target temperature. * **Parameters:** **mode** – Operating mode to set. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_temperature(temperature: float) → None Set the target temperature, keeping the current operating mode. **WARNING:** This method only has an effect when the zone is in `ThermoZoneMode.MANUAL` mode. When the zone is in any other mode (e.g. `AUTO`, `JOLLY`), the server accepts the request without error but silently discards the new setpoint. Use [`async_set_config()`](#aiocamedomotic.models.ThermoZone.async_set_config) with `mode=ThermoZoneMode.MANUAL` to guarantee that the setpoint is applied. * **Parameters:** **temperature** – Target temperature in degrees Celsius. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* dehumidifier_enabled *: bool* Whether the dehumidifier is enabled for this zone. ##### *property* dehumidifier_setpoint *: float | None* Dehumidifier humidity setpoint in percent, or None if not set. ##### *property* fan_speed *: [ThermoZoneFanSpeed](#aiocamedomotic.models.ThermoZoneFanSpeed)* Fan speed setting for the thermoregulation zone. Returns `ThermoZoneFanSpeed.UNKNOWN` for unrecognized values. ##### *property* floor_ind *: int | None* Floor index of the thermoregulation zone. ##### *property* leaf *: bool* Whether this is an actual zone (leaf node) in the hierarchy. ##### *property* mode *: [ThermoZoneMode](#aiocamedomotic.models.ThermoZoneMode)* Operating mode of the thermoregulation zone. Returns `ThermoZoneMode.UNKNOWN` for unrecognized mode values. ##### *property* name *: str* Name of the thermoregulation zone. ##### *property* profile *: [ThermoProfile](#aiocamedomotic.models.ThermoProfile)* Weekly setpoint-level profile (typed view). Parsed fresh from `raw_data` on every access (never cached). * **Raises:** **ValueError** – If `raw_data` lacks a well-formed `profile_data` value. ##### *property* profile_data *: list[str]* Weekly setpoint-level profile (raw wire format, copied). Eight strings — Monday..Sunday plus the JOLLY profile as the 8th row — of 96 characters each (one per quarter hour of day). Each character is a digit `1`-`5` selecting one of the five setpoint levels shown in the official app. For a typed view use [`profile`](#aiocamedomotic.models.ThermoZone.profile). Read-only: the thermo profile set command has never been observed in captured traffic, so writing a profile back to the zone is not yet supported by the library. Returns a copy: mutating the returned list does not affect `raw_data`. Returns an empty list if the zone data carries no profile (push updates do not include it). ##### *property* room_ind *: int | None* Room index of the thermoregulation zone. ##### *property* season *: [ThermoZoneSeason](#aiocamedomotic.models.ThermoZoneSeason)* Season setting for the thermoregulation zone. Returns `ThermoZoneSeason.UNKNOWN` for unrecognized season values. ##### *property* set_point *: float* Target temperature in degrees Celsius. ##### *property* status *: [ThermoZoneStatus](#aiocamedomotic.models.ThermoZoneStatus)* Status of the thermoregulation zone (OFF or ON). ##### *property* t1 *: float | None* Temperature sensor 1 reading in degrees Celsius, or None. ##### *property* t2 *: float | None* Temperature sensor 2 reading in degrees Celsius, or None. ##### *property* t3 *: float | None* Temperature sensor 3 reading in degrees Celsius, or None. ##### *property* temperature *: float* Current temperature in degrees Celsius. Handles both `temp` (from list responses) and `temp_dec` (from status indications) field names. #### *class* aiocamedomotic.models.ThermoZoneFanSpeed(\*values) Fan speed setting for a thermoregulation zone. Allowed values are: : - OFF (0) - SLOW (1) - MEDIUM (2) - FAST (3) - AUTO (4) #### *class* aiocamedomotic.models.ThermoZoneMode(\*values) Operating mode of a thermoregulation zone. Allowed values are: : - OFF (0) - MANUAL (1) - AUTO (2) - JOLLY (3) #### *class* aiocamedomotic.models.ThermoZoneSeason(\*values) Season setting for a thermoregulation zone. Allowed values are: : - PLANT_OFF (“plant_off”) - WINTER (“winter”) - SUMMER (“summer”) #### *class* aiocamedomotic.models.ThermoZoneStatus(\*values) Status of a thermoregulation zone. Allowed values are: : - OFF (0) - ON (1) #### *class* aiocamedomotic.models.ThermoZoneUpdate(raw_data: dict[str, Any]) Typed update for a thermostat zone (`thermo_zone_info_ind` / `thermo_update_ind`). ##### *property* act_id *: int* Zone actuator ID. ##### *property* dehumidifier_enabled *: bool* Whether the dehumidifier is enabled. ##### *property* dehumidifier_setpoint *: float | None* Dehumidifier humidity setpoint in percent, or None if not present. ##### *property* fan_speed *: [ThermoZoneFanSpeed](#aiocamedomotic.models.ThermoZoneFanSpeed)* Fan speed setting (OFF, SLOW, MEDIUM, FAST, AUTO). ##### *property* floor_ind *: int* Floor index. ##### *property* mode *: [ThermoZoneMode](#aiocamedomotic.models.ThermoZoneMode)* Operating mode (OFF, MANUAL, AUTO, JOLLY). ##### *property* room_ind *: int* Room index. ##### *property* season *: [ThermoZoneSeason](#aiocamedomotic.models.ThermoZoneSeason)* Season setting (PLANT_OFF, WINTER, SUMMER). ##### *property* set_point *: float* Target temperature in degrees Celsius (converted from `set_point`). ##### *property* status *: [ThermoZoneStatus](#aiocamedomotic.models.ThermoZoneStatus)* Zone status (OFF, ON). ##### *property* t1 *: float | None* Temperature sensor 1 reading in degrees Celsius. ##### *property* t2 *: float | None* Temperature sensor 2 reading in degrees Celsius. ##### *property* t3 *: float | None* Temperature sensor 3 reading in degrees Celsius. ##### *property* temperature *: float* Current temperature in degrees Celsius (converted from `temp_dec`). #### *class* aiocamedomotic.models.Timer(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) Timer entity in the CameDomotic API. Represents a scheduling timer with time-based activation windows. Supports enabling/disabling, day-of-week toggling, and timetable configuration via the CAME API. The `days` field is a 7-bit bitmask (bit 0 = Monday, …, bit 6 = Sunday). For example, `days=85` (binary `1010101`) means Monday, Wednesday, Friday, and Sunday. * **Raises:** **ValueError** – If `name` or `id` keys are missing from the input data or the auth argument is not an instance of `Auth`. ##### *property* active_days *: list[str]* Human-readable names of the days the timer is active on. ##### *async* async_disable() → None Disable the timer. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_disable_day(day: int) → None Disable the timer for a specific day of the week. * **Parameters:** **day** – Day index (0=Monday, 1=Tuesday, …, 6=Sunday). * **Raises:** * **ValueError** – If *day* is not in range 0-6. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_enable() → None Enable the timer. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_enable_day(day: int) → None Enable the timer for a specific day of the week. * **Parameters:** **day** – Day index (0=Monday, 1=Tuesday, …, 6=Sunday). * **Raises:** * **ValueError** – If *day* is not in range 0-6. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_timetable(slots: list[tuple[int, int, int] | None]) → None Set the timer’s timetable. Sends the complete timetable to the server. The list must contain exactly 4 entries — one per available slot. Use `None` for empty slots. * **Parameters:** **slots** – List of 4 entries. Each entry is either a `(hour, min, sec)` tuple for an active slot, or `None` for an empty slot. * **Raises:** * **ValueError** – If *slots* does not contain exactly 4 entries. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *property* bars *: int* Number of timetable bars reported by the server. ##### *property* days *: int* Bitmask of active days (bit 0 = Monday, …, bit 6 = Sunday). ##### *property* enabled *: bool* Whether the timer is globally enabled. ##### *property* id *: int* Unique timer identifier. ##### is_active_on_day(day_index: int) → bool Check whether the timer is active on a specific day. * **Parameters:** **day_index** – Day index (0=Monday, 1=Tuesday, …, 6=Sunday). * **Returns:** `True` if the timer is scheduled for this day, `False` otherwise (including when *day_index* is out of range). ##### *property* name *: str* Display name of the timer. ##### *property* timetable *: list[[TimerTimeSlot](#aiocamedomotic.models.TimerTimeSlot)]* Scheduled time slots. Malformed entries are skipped with a warning. #### *class* aiocamedomotic.models.TimerTimeSlot(raw_data: dict[str, Any]) A single time window within a timer’s timetable. On some firmware versions, the `stop` and `active` fields may be absent. Properties return `None` for missing optional fields. * **Parameters:** **raw_data** – Dictionary from a single timetable array entry. * **Raises:** **ValueError** – If `index` or `start` keys are missing. ##### *property* active *: bool | None* Whether this time window is active, or `None` if absent. ##### *property* index *: int* Zero-based slot position in the timetable (0-3). ##### *property* start_hour *: int* Start time hour (0-23). Defaults to 0 if missing. ##### *property* start_min *: int* Start time minute (0-59). Defaults to 0 if missing. ##### *property* start_sec *: int* Start time second (0-59). Defaults to 0 if missing. ##### *property* stop_hour *: int | None* Stop time hour, or `None` if the `stop` field is absent. ##### *property* stop_min *: int | None* Stop time minute, or `None` if the `stop` field is absent. ##### *property* stop_sec *: int | None* Stop time second, or `None` if the `stop` field is absent. #### *class* aiocamedomotic.models.TimerUpdate(raw_data: dict[str, Any]) Typed update for a timer (`timer_info_ind` / `timer_update_ind`). ##### *property* bars *: int* Number of timetable bars reported by the server. ##### *property* days *: int* Days bitmask (bit 0 = Monday, …, bit 6 = Sunday). ##### *property* device_id *: int | None* For timers the primary ID is `id`. ##### *property* enabled *: bool* Whether the timer is enabled. ##### *property* id *: int* Timer ID. ##### *property* timetable *: list[dict[str, Any]]* Raw timetable entries from the update. #### *class* aiocamedomotic.models.TopologyFloor(id: int, name: str, rooms: list[[TopologyRoom](#aiocamedomotic.models.TopologyRoom)]) A floor in the plant topology. Contains the list of rooms discovered on this floor. ##### id *: int* Numeric identifier of the floor (`floor_ind`). ##### name *: str* Human-readable name of the floor. ##### rooms *: list[[TopologyRoom](#aiocamedomotic.models.TopologyRoom)]* Rooms belonging to this floor. #### *class* aiocamedomotic.models.TopologyRoom(id: int, name: str) A room in the plant topology. Lightweight representation used by [`PlantTopology`](#aiocamedomotic.models.PlantTopology) to describe the building structure independently of any specific device type. ##### id *: int* Numeric identifier of the room (`room_ind`). ##### name *: str* Human-readable name of the room. #### *class* aiocamedomotic.models.UpdateIndicator(\*values) Known status-update indication cmd_names from the CAME API. These identify the type of state change in a `status_update_resp` result item. Some indicators have two variants: one observed in real API traffic and one documented in API_reference.md. Both are mapped for firmware compatibility. Values: : - LIGHT (“light_switch_ind”) - OPENING (“opening_move_ind”) - RELAY (“relay_status_ind”) - THERMOSTAT (“thermo_zone_info_ind”) - DIGITAL_INPUT (“digitalin_status_ind”) - ANALOG_INPUT (“analogin_status_ind”) - SCENARIO_STATUS (“scenario_status_ind”) - SCENARIO_ACTIVATION (“scenario_activation_ind”) - ENERGY_METER (“meter_instant_power_ind”) - LOADSCTRL_METER (“loadsctrl_meter_ind”) - LOADSCTRL_RELAY (“loadsctrl_relay_ind”) - PLANT (“plant_update_ind”) - LIGHT_LEGACY (“light_update_ind”) - OPENING_LEGACY (“opening_update_ind”) - THERMOSTAT_LEGACY (“thermo_update_ind”) - RELAY_LEGACY (“relay_update_ind”) - DIGITAL_INPUT_LEGACY (“digitalin_update_ind”) - ANALOG_INPUT_LEGACY (“analogin_update_ind”) - TIMER (“timer_info_ind”) - SCENARIO_USER_LEGACY (“scenario_user_ind”) - TIMER_LEGACY (“timer_update_ind”) #### *class* aiocamedomotic.models.UpdateList(updates: UserList[dict[str, Any]] | None = None) Chronological list of status updates from the CameDomotic API. Extends `UserList` to maintain backward compatibility: iterating yields raw `dict` objects. Additional methods provide typed access and filtering. ##### get_by_device_type(device_type: [DeviceType](#aiocamedomotic.const.DeviceType)) → list[dict[str, Any]] Return raw update dicts filtered to the given device type. * **Parameters:** **device_type** – The [`DeviceType`](#aiocamedomotic.const.DeviceType) to filter by. * **Returns:** A list of raw update dicts whose `cmd_name` maps to *device_type*. ##### get_typed_by_device_type(device_type: [DeviceType](#aiocamedomotic.const.DeviceType)) → list[[DeviceUpdate](#aiocamedomotic.models.DeviceUpdate)] Parse and filter updates by device type. * **Parameters:** **device_type** – The [`DeviceType`](#aiocamedomotic.const.DeviceType) to filter by. * **Returns:** Typed [`DeviceUpdate`](#aiocamedomotic.models.DeviceUpdate) instances whose [`device_type`](#aiocamedomotic.models.DeviceUpdate.device_type) matches *device_type*. ##### get_typed_updates() → list[[DeviceUpdate](#aiocamedomotic.models.DeviceUpdate)] Parse all updates into typed [`DeviceUpdate`](#aiocamedomotic.models.DeviceUpdate) objects. * **Returns:** A list of [`DeviceUpdate`](#aiocamedomotic.models.DeviceUpdate) subclass instances, one per raw update dict. ##### *property* has_plant_update *: bool* Whether any update in this list is a `plant_update_ind`. When `True`, the consumer should discard all cached devices and re-fetch them from the server. #### *class* aiocamedomotic.models.User(raw_data: dict[str, Any], auth: [Auth](#aiocamedomotic.auth.Auth)) User in the CAME Domotic API. * **Raises:** **ValueError** – If name key is missing from the input data the auth argument is not an instance of the expected Auth class. ##### *async* async_change_password(current_password: str, new_password: str) → None Change the password of this user on the CAME Domotic server. * **Parameters:** * **current_password** (*str*) – The user’s current password. * **new_password** (*str*) – The desired new password. **NOTE:** Changing the password does not invalidate existing active sessions for that user — they remain valid until they expire. The new password will be required at the next login. If the changed user is the currently authenticated user, the stored credentials are updated automatically in the active session — no additional action is required. * **Raises:** * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error or the password change is rejected (`sl_user_pwd_change_ack_reason` is non-zero). ##### *async* async_delete() → None Delete this user from the CAME Domotic server. Sends a delete-user request to the server for the user identified by this object’s `name` property. * **Raises:** * **ValueError** – If this user is the currently authenticated user. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – If the server returns an error. ##### *async* async_set_as_current_user(password: str) → None Set the user as the current user in the CAME Domotic API session. * **Parameters:** **password** (*str*) – Password of the user. * **Raises:** [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – If the authentication fails. **NOTE:** This method logs out the current user and logs in with the new user. If login with the new credentials fails, a `CameDomoticAuthError` is raised and the previous credentials are restored so the API client remains connected as the original user. ##### *property* name *: str* Name of the user. #### *class* aiocamedomotic.models.WeeklyProfile(rows: Sequence[Sequence[int]]) Immutable weekly schedule grid (abstract base). Concrete subclasses ([`LoadsCtrlProfile`](#aiocamedomotic.models.LoadsCtrlProfile), [`ThermoProfile`](#aiocamedomotic.models.ThermoProfile)) define the grid shape via [`DAYS`](#aiocamedomotic.models.WeeklyProfile.DAYS) and [`WIRE_SLOTS_PER_DAY`](#aiocamedomotic.models.WeeklyProfile.WIRE_SLOTS_PER_DAY); all parsing, reading, and editing logic lives here. Instances are immutable value objects: editing methods ([`with_level()`](#aiocamedomotic.models.WeeklyProfile.with_level), [`with_day_copied()`](#aiocamedomotic.models.WeeklyProfile.with_day_copied)) return a **new** instance, and equality/hashing are by value (two profiles of the same class with the same grid are equal). Construct instances via [`from_wire()`](#aiocamedomotic.models.WeeklyProfile.from_wire) or [`constant()`](#aiocamedomotic.models.WeeklyProfile.constant). The public API speaks in **hours** (the official app edits per hour); the internal grid keeps the wire resolution, so serializing an unedited profile reproduces the server bytes exactly. ##### DAYS *: ClassVar[tuple[[ProfileDay](#aiocamedomotic.models.ProfileDay), ...]]* The rows of the grid, in wire order. ##### LEVELS *: ClassVar[frozenset[int]]* *= frozenset({1, 2, 3, 4, 5})* Allowed level values (the five levels shown in the official app). ##### WIRE_SLOTS_PER_DAY *: ClassVar[int]* Number of wire slots per day row (a multiple of 24). ##### *classmethod* constant(level: int) → Self Create a profile with every slot of every row set to `level`. * **Raises:** * **TypeError** – If called on the abstract `WeeklyProfile` class. * **ValueError** – If `level` is not in [`LEVELS`](#aiocamedomotic.models.WeeklyProfile.LEVELS). ##### *classmethod* from_wire(data: Sequence[str]) → Self Parse a profile from its raw wire format. The wire format is a list of digit strings, one per row of [`DAYS`](#aiocamedomotic.models.WeeklyProfile.DAYS) in order, each of exactly [`WIRE_SLOTS_PER_DAY`](#aiocamedomotic.models.WeeklyProfile.WIRE_SLOTS_PER_DAY) characters, each character a digit in [`LEVELS`](#aiocamedomotic.models.WeeklyProfile.LEVELS). Guarantee: `cls.from_wire(x).to_wire() == x` byte-for-byte — nothing is normalized or reinterpreted. On profile types with sub-hour wire slots, a row whose hours are not uniform (different levels within the same hour) is unexpected — the official app edits per hour — so a warning is logged, but the data is accepted and preserved exactly. * **Parameters:** **data** – The raw profile rows, e.g. from a server response. * **Raises:** * **TypeError** – If called on the abstract `WeeklyProfile` class. * **ValueError** – If the shape or any character is invalid. ##### level_at(day: [ProfileDay](#aiocamedomotic.models.ProfileDay) | int, at: time | datetime | int) → int Return the level active on `day` at the given moment. * **Parameters:** * **day** – The profile row to read (a [`ProfileDay`](#aiocamedomotic.models.ProfileDay) or its integer value). * **at** – The moment of day: an `int` hour (0-23), a `datetime.time`, or a `datetime.datetime` (its time of day is used; the date is **not** used to pick `day`). Any time is accepted — it resolves to the containing wire slot, so no hour alignment is required for reads. * **Raises:** **ValueError** – If `day` is not a row of this profile type or `at` is not a valid moment of day. ##### spans(day: [ProfileDay](#aiocamedomotic.models.ProfileDay) | int) → list[[ProfileSpan](#aiocamedomotic.models.ProfileSpan)] Return `day`’s schedule as runs of consecutive equal levels. Each [`ProfileSpan`](#aiocamedomotic.models.ProfileSpan) is a half-open `[start, end)` range; the last span’s `end` is `time(0)`, meaning “through midnight”. For app-written data the boundaries fall on whole hours. * **Raises:** **ValueError** – If `day` is not a row of this profile type. ##### to_wire() → list[str] Serialize to the raw wire format (a fresh list of digit strings). For a profile obtained via [`from_wire()`](#aiocamedomotic.models.WeeklyProfile.from_wire) and not edited since, this returns exactly the original input. ##### with_day_copied(source: [ProfileDay](#aiocamedomotic.models.ProfileDay) | int, to: [ProfileDay](#aiocamedomotic.models.ProfileDay) | int | Iterable[[ProfileDay](#aiocamedomotic.models.ProfileDay) | int]) → Self Return a copy with `source`’s whole row copied over `to`. The “copy this day to other days” operation. Rows are copied at wire resolution, so the copy is lossless. The original profile is not modified. * **Parameters:** * **source** – The row to copy from. * **to** – The row(s) to copy onto: a single [`ProfileDay`](#aiocamedomotic.models.ProfileDay) (or its integer value) or an iterable of them. * **Raises:** **ValueError** – If `source` or any target is not a row of this profile type. ##### with_level(level: int, \*, days: [ProfileDay](#aiocamedomotic.models.ProfileDay) | int | Iterable[[ProfileDay](#aiocamedomotic.models.ProfileDay) | int] | None = None, start: time | int = 0, end: time | int | None = None) → Self Return a copy with `[start, end)` on `days` set to `level`. The original profile is not modified. The full week is always kept (and later sent to the server) — this method only chooses which cells of the copy get the new level. * **Parameters:** * **level** – The level to set (must be in [`LEVELS`](#aiocamedomotic.models.WeeklyProfile.LEVELS)). * **days** – The rows to change: a single [`ProfileDay`](#aiocamedomotic.models.ProfileDay) (or its integer value), an iterable of them, or `None` (the default) for **every** row of this profile type — on thermo profiles that includes `ProfileDay.JOLLY`; pass `WEEKDAYS` to target Monday..Sunday only. * **start** – Start hour, inclusive: an `int` (0-23) or a whole-hour `datetime.time`. Defaults to `0` (midnight). * **end** – End hour, exclusive: an `int` (1-24), a whole-hour `datetime.time` (`time(0)` means end of day), or `None` (the default) for end of day. Edits are hour-based, so `start`/`end` must be whole hours (no rounding is applied) and spans cannot cross midnight — split such an edit into two calls. * **Raises:** **ValueError** – If `level`, `days`, `start`, or `end` are invalid, or if `start >= end`. #### aiocamedomotic.models.get_update_device_type(update: dict[str, Any]) → [DeviceType](#aiocamedomotic.const.DeviceType) | None Return the device type for a status update dict, or None if unknown. * **Parameters:** **update** – A single update dict from the `status_update_resp` result array. Must contain a `cmd_name` key. * **Returns:** The corresponding [`DeviceType`](#aiocamedomotic.const.DeviceType), or `None` if the `cmd_name` is not recognized. #### aiocamedomotic.models.parse_update(raw: dict[str, Any]) → [DeviceUpdate](#aiocamedomotic.models.DeviceUpdate) Parse a raw update dict into the appropriate typed [`DeviceUpdate`](#aiocamedomotic.models.DeviceUpdate) subclass. * **Parameters:** **raw** – A single update dict from the `status_update_resp` result array. * **Returns:** A typed [`DeviceUpdate`](#aiocamedomotic.models.DeviceUpdate) subclass instance. If the `cmd_name` is not recognized, a generic [`DeviceUpdate`](#aiocamedomotic.models.DeviceUpdate) is returned so that the consumer can still access `raw_data`. ### Constants Constants for the CAME Domotic API. #### *class* aiocamedomotic.const.AckErrorCode(\*values) ACK error codes returned by the CAME Domotic server. Each member carries a human-readable `message` and an `is_auth` flag indicating whether the error is authentication-related. Because `AckErrorCode` is an `IntEnum`, members compare equal to their integer value (e.g. `AckErrorCode.INVALID_USER == 1`). Values: - `INVALID_USER` (1): Invalid user. **[auth]** - `TOO_MANY_SESSIONS` (3): Too many sessions during login. **[auth]** - `JSON_SYNTAX_ERROR` (4): Error occurred in JSON Syntax. - `NO_SESSION_COMMAND_TAG` (5): No session layer command tag. - `UNRECOGNIZED_SESSION_COMMAND` (6): Unrecognized session layer command. - `NO_CLIENT_ID` (7): No client ID in request. - `WRONG_CLIENT_ID` (8): Wrong client ID in request. - `WRONG_APPLICATION_COMMAND` (9): Wrong application command. - `NO_REPLY` (10): No reply to application command, maybe service down. - `WRONG_APPLICATION_DATA` (11): Wrong application data. ##### *property* is_auth *: bool* Whether this error code indicates an authentication failure. ##### *property* message *: str* Human-readable error message for this ACK code. #### *class* aiocamedomotic.const.DeviceType(\*values) Device type IDs used by the CAME ETI/Domo system. Each device in the CAME Domotic system is associated with one of these type identifiers. Not all device types are currently supported by this library. Negative IDs are library-specific: they identify entity kinds that the CAME API does not assign a numeric type to. Values: : - LOADSCTRL_RELAY (-5) - LOADSCTRL_METER (-4) - ANALOG_INPUT (-3) - ENERGY_SENSOR (-2) - ANALOG_SENSOR (-1) - LIGHT (0) - OPENING (1) - THERMOSTAT (2) - PAGE (3) - SCENARIO (4) - CAMERA (5) - SECURITY_PANEL (6) - SECURITY_AREA (7) - SECURITY_SCENARIO (8) - SECURITY_INPUT (9) - SECURITY_OUTPUT (10) - GENERIC_RELAY (11) - GENERIC_TEXT (12) - SOUND_ZONE (13) - DIGITAL_INPUT (14) - TIMER (15) #### *class* aiocamedomotic.const.UpdateIndicator(\*values) Known status-update indication cmd_names from the CAME API. These identify the type of state change in a `status_update_resp` result item. Some indicators have two variants: one observed in real API traffic and one documented in API_reference.md. Both are mapped for firmware compatibility. Values: : - LIGHT (“light_switch_ind”) - OPENING (“opening_move_ind”) - RELAY (“relay_status_ind”) - THERMOSTAT (“thermo_zone_info_ind”) - DIGITAL_INPUT (“digitalin_status_ind”) - ANALOG_INPUT (“analogin_status_ind”) - SCENARIO_STATUS (“scenario_status_ind”) - SCENARIO_ACTIVATION (“scenario_activation_ind”) - ENERGY_METER (“meter_instant_power_ind”) - LOADSCTRL_METER (“loadsctrl_meter_ind”) - LOADSCTRL_RELAY (“loadsctrl_relay_ind”) - PLANT (“plant_update_ind”) - LIGHT_LEGACY (“light_update_ind”) - OPENING_LEGACY (“opening_update_ind”) - THERMOSTAT_LEGACY (“thermo_update_ind”) - RELAY_LEGACY (“relay_update_ind”) - DIGITAL_INPUT_LEGACY (“digitalin_update_ind”) - ANALOG_INPUT_LEGACY (“analogin_update_ind”) - TIMER (“timer_info_ind”) - SCENARIO_USER_LEGACY (“scenario_user_ind”) - TIMER_LEGACY (“timer_update_ind”) ### Utilities #### *async* aiocamedomotic.utils.async_is_came_endpoint(host: str, websession: ClientSession | None = None, timeout: int = 10) → bool Check whether a host exposes the CAME Domotic API endpoint. Performs a credential-free HTTP GET on the CAME API URL to determine if the host is a CAME ETI/Domo server. Suitable for network autodiscovery alongside `CAME_MAC_PREFIXES`. * **Parameters:** * **host** – IP address or hostname of the device to check (e.g., `"192.168.1.100"`, `"came-server.local"`). * **websession** – Optional `aiohttp.ClientSession` to reuse. When provided, the caller retains ownership and the session will **not** be closed by this function. When omitted, a temporary session is created and closed automatically. * **timeout** – HTTP request timeout in seconds (default: 10). * **Returns:** `True` if the host responds with HTTP 200 on the CAME API endpoint, `False` otherwise (network error, timeout, wrong status, etc.). Anonymization utilities for HTTP traffic logging. This module provides automatic redaction of sensitive fields in CAME Domotic API request and response payloads, enabling safe sharing of traffic logs for debugging purposes. The traffic logger (`aiocamedomotic.traffic`) is a child of the main library logger and can be configured independently: ```default import logging logging.getLogger("aiocamedomotic.traffic").setLevel(logging.DEBUG) ``` #### aiocamedomotic.anonymizer.TRAFFIC_LOGGER *= * Dedicated logger for HTTP traffic. Enable at DEBUG level to see anonymized request/response payloads with elapsed times. ### Errors This module contains the exceptions that can be raised by the CAME Domotic API. Exception hierarchy and suggested Home Assistant mapping: - [`CameDomoticServerNotFoundError`](#aiocamedomotic.errors.CameDomoticServerNotFoundError) → `ConfigEntryNotReady` (host unreachable, transient) - [`CameDomoticAuthError`](#aiocamedomotic.errors.CameDomoticAuthError) → `ConfigEntryAuthFailed` (bad credentials, permanent — triggers reauth flow) - [`CameDomoticServerTimeoutError`](#aiocamedomotic.errors.CameDomoticServerTimeoutError) → `ConfigEntryNotReady` (request timeout, transient) - [`CameDomoticServerError`](#aiocamedomotic.errors.CameDomoticServerError) (other ACK codes) → log and re-raise #### *exception* aiocamedomotic.errors.CameDomoticAuthError Raised when there is an authentication error with the remote server. #### *exception* aiocamedomotic.errors.CameDomoticError Base exception class for the CAME Domotic package. #### *exception* aiocamedomotic.errors.CameDomoticServerError Raised if an error occurs while interacting with the remote CAME Domotic server. ##### *static* create_ack_error(ack_code: int) → [CameDomoticError](#aiocamedomotic.errors.CameDomoticError) Create appropriate exception based on ACK error code. * **Parameters:** **ack_code** (*int*) – The ACK error code from the server. * **Returns:** Appropriate exception instance based on error code. * **Return type:** [CameDomoticError](#aiocamedomotic.errors.CameDomoticError) ##### *static* format_ack_error(ack_code: int) → str Formats the ack code in a human-readable format. * **Parameters:** **ack_code** (*int*) – The ACK error code from the server. * **Returns:** The formatted error message. * **Return type:** str #### *exception* aiocamedomotic.errors.CameDomoticServerNotFoundError Raised when the specified host is not available. #### *exception* aiocamedomotic.errors.CameDomoticServerTimeoutError Raised when a request to the CAME Domotic server times out. This exception indicates a transient failure. When using this library with Home Assistant, it should be mapped to `ConfigEntryNotReady` to allow the integration to retry with exponential backoff. See also the exception hierarchy mapping for Home Assistant integrations: - [`CameDomoticServerNotFoundError`](#aiocamedomotic.errors.CameDomoticServerNotFoundError) → `ConfigEntryNotReady` (host unreachable, transient) - [`CameDomoticAuthError`](#aiocamedomotic.errors.CameDomoticAuthError) → `ConfigEntryAuthFailed` (bad credentials, permanent — triggers reauth flow) - [`CameDomoticServerTimeoutError`](#aiocamedomotic.errors.CameDomoticServerTimeoutError) → `ConfigEntryNotReady` (request timeout, transient) - [`CameDomoticServerError`](#aiocamedomotic.errors.CameDomoticServerError) (other ACK codes) → log and re-raise ### Auth module This module manages the HTTP interaction with the CAME Domotic API. **NOTE:** As a consumer of the CAME Domotic library, **it’s quite unlikely that you will need to use this class directly**: you should use the `CameDomoticAPI` and the CameEntity classes instead. In case of special needs, consider requesting the implementation of the desired feature in the CAME Domotic library, or forking the library and implement the feature yourself. #### *class* aiocamedomotic.auth.Auth(websession: ClientSession, host: str, username: str, password: str, \*, close_websession_on_disposal: bool = False) Class to make authenticated requests to the CAME Domotic API server. Security features: - **Credential protection** — username and password are encrypted in memory using Fernet symmetric encryption with a runtime-generated key. Credentials are explicitly cleared on disposal and as a safety net on garbage collection. **NOTE:** This class is not meant to be used directly, but through the `CameDomoticAPI` class. To create an instance of this class, use the factory method `async_create`. ##### *async classmethod* async_create(websession: ClientSession, host: str, username: str, password: str, \*, close_websession_on_disposal: bool = False, command_timeout: int = 30) → [Auth](#aiocamedomotic.auth.Auth) Create an Auth instance. * **Parameters:** * **websession** (*ClientSession*) – the aiohttp client session. * **host** (*str*) – the host of the CAME Domotic server. * **username** (*str*) – the username to use for the authentication. * **password** (*str*) – the password to use for the authentication. * **close_websession_on_disposal** (*bool* *,* *optional*) – whether to close the websession when disposing the Auth instance (default: False). * **command_timeout** (*int* *,* *optional*) – the default timeout in seconds for commands sent to the server (default: 30s). * **Raises:** [**CameDomoticServerNotFoundError**](#aiocamedomotic.errors.CameDomoticServerNotFoundError) – if the host doesn’t respond to an HTTP request or doesn’t expose the CAME Domotic API endopoint. * **Returns:** the Auth instance. * **Return type:** [Auth](#aiocamedomotic.auth.Auth) **NOTE:** The session is not logged in until the first request is made. ##### *async* async_dispose() → None Dispose the Auth instance, eventually logging out if needed. This method also explicitly clears sensitive attributes (username, password, and cipher_suite) to enhance security when the Auth instance is disposed. ##### *async* async_get_valid_client_id() → str Get a valid client ID, eventually logging in if needed. * **Returns:** the client ID. * **Return type:** str * **Raises:** [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – if an error occurs during the login. ##### *async* async_keep_alive() → None Keep the session alive, eventually logging in again if needed. * **Raises:** * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – if an error occurs during the keep-alive request. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – if an error occurs during the login. ##### *async* async_login() → None Login to the CAME Domotic server. * **Raises:** [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – if an error occurs during the login. ##### *async* async_logout() → None Logout from the CAME Domotic server. * **Raises:** [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – if an error occurs during the logout. ##### *async static* async_raise_for_status_and_ack(response: ClientResponse) → None Check the response status and raise an error if necessary. * **Parameters:** **response** (*ClientResponse*) – the response. * **Raises:** * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – if there is an error interacting with the remote CAME Domotic server. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – if there is an authentication error with the remote CAME Domotic server. ##### *async* async_send_command(command: dict[str, Any], \*, response_command: str | None = None, timeout: int | None = None, skip_ack_check: bool = False, command_type: str = 'sl_data_req', additional_payload: dict[str, Any] | None = None, \_caller_holds_lock: bool = False) → dict[str, Any] Send a command to the CAME Domotic server. * **Parameters:** * **command** (*dict*) – the command to send. * **response_command** (*str* *,* *optional*) – expected response command name to validate against the server response (default: None). * **timeout** (*int* *|* *None* *,* *optional*) – the timeout in seconds. If None, uses the instance-level `command_timeout` (default: 30s). * **skip_ack_check** (*bool* *,* *optional*) – whether to skip the ACK check (default: False). * **command_type** (*str* *,* *optional*) – the command type to send (default: “sl_data_req”). * **additional_payload** (*dict* *,* *optional*) – additional key-value pairs to include in the request payload (default: None). * **\_caller_holds_lock** (*bool* *,* *optional*) – when True, the caller already holds `self._lock` and has validated the session, so this method uses `self.client_id` directly instead of calling `async_get_valid_client_id()` (which would deadlock). For internal use only (default: False). * **Returns:** the JSON response from the server. * **Return type:** dict * **Raises:** * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – if an error occurs during the command. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – if there is an authentication error with the remote CAME Domotic server. ##### *async* async_validate_host(timeout: int = 10) → None Validate the host asynchronously using aiohttp. * **Parameters:** **timeout** (*int* *,* *optional*) – the timeout in seconds (default: 10s). * **Raises:** [**CameDomoticServerNotFoundError**](#aiocamedomotic.errors.CameDomoticServerNotFoundError) – if the host doesn’t respond to an HTTP request or doesn’t expose the CAME Domotic API endopoint. ##### backup_auth_credentials() → tuple[bytes | None, bytes | None, str, float, int, int] Backup the current authentication credentials. ##### *static* create_cypher_suite() → Fernet Create a cypher suite. ##### *property* current_username *: str | None* Return the decrypted username for the current session, or None. * **Returns:** The plaintext username, or `None` if the cipher suite has not been initialised (e.g. after `async_dispose`). * **Return type:** str | None **NOTE:** Usernames are not secret — they appear in plaintext in all API payloads. This property is intended for internal comparisons (e.g. preventing deletion of the current user). Avoid logging its return value unnecessarily. ##### get_endpoint_url() → str Get the CAME Domotic endpoint URL. * **Returns:** the endpoint URL. * **Return type:** str ##### is_session_valid() → bool Check whether the session is still valid or not. ##### restore_auth_credentials(backup_state: tuple[bytes | None, bytes | None, str, float, int, int]) → None Restore authentication credentials from a backup. * **Parameters:** **backup_state** (*tuple*) – Username and password. ##### update_auth_credentials(username: str, password: str) → None Update the authentication credentials. * **Parameters:** * **username** (*str*) – New username. * **password** (*str*) – New password. #### aiocamedomotic.auth.handle_came_domotic_errors(func: \_F) → \_F Decorator to handle CAME Domotic API errors. The decorator catches the following exceptions: - aiohttp.ClientResponseError: for HTTP errors (4xx, 5xx) - TimeoutError: for timeouts (asyncio.TimeoutError, ServerTimeoutError) - aiohttp.ClientError: for other network-related errors - CameDomoticAuthError: for authentication errors - any other exception: for unforeseen errors * **Raises:** * [**CameDomoticServerError**](#aiocamedomotic.errors.CameDomoticServerError) – in case of any of the above errors. * [**CameDomoticAuthError**](#aiocamedomotic.errors.CameDomoticAuthError) – in case of authentication error.