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.
Note
The examples below assume the library is installed (pip install
aiocamedomotic). Unless otherwise noted, all code runs inside an
async with block:
import asyncio
from aiocamedomotic import CameDomoticAPI
from aiocamedomotic.models import (
AnalogSensorType, DeviceType, DigitalInputStatus, LightStatus,
LightType, OpeningStatus, RelayStatus, ScenarioStatus,
ServerFeature, ThermoZoneFanSpeed,
ThermoZoneMode, ThermoZoneSeason, ThermoZoneStatus,
Timer, TimerTimeSlot, TimerUpdate,
DeviceUpdate, LightUpdate, OpeningUpdate, RelayUpdate,
ThermoZoneUpdate, ScenarioUpdate, DigitalInputUpdate, PlantUpdate,
)
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:
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:
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:
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 classCameDomoticServerNotFoundError— host unreachable (transient)CameDomoticAuthError— bad credentials or too many sessionsCameDomoticServerError— other server errorsCameDomoticServerTimeoutError— 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:
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:
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:
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.):
server_info = await api.async_get_server_info()
for feature in server_info.features:
print(f"Feature: {feature}")
Example output:
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. You can compare
entries against enum members to decide which device APIs to call:
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()
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:
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:
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:
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:
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:
# 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:
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.
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).
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.
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()
Thermoregulation zones
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:
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:
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().
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:
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:
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:
# 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.
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:
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:
# 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. The same physical sensor
may appear in both endpoints.
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:
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:
# 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:
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:
ID: 31, Name: Garden Pump, Status: RelayStatus.ON
ID: 32, Name: Gate Motor, Status: RelayStatus.OFF
Finding a specific relay:
# 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:
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:
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:
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:
# 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:
Bit: 6 5 4 3 2 1 0
Day: Sun Sat Fri Thu Wed Tue Mon
Common values:
1— Monday only15— 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:
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
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 timestop_hour,stop_min,stop_sec— the stop time (Noneon some firmware versions)active— whether the slot is individually active (Noneon some firmware versions)
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:
Slot 0: 10:00:00 → 18:30:00
Slot 2: 22:00:00 → N/A
Note
The stop and active fields mayb be not
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:
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.
# 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:
# 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:
# 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:
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())
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 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:
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}] {update.name} (ID: {update.device_id})")
# Brief pause to avoid tight looping on rapid server responses
await asyncio.sleep(1)
Example output:
[DeviceType.LIGHT] Living Room Chandelier (ID: 1)
[DeviceType.OPENING] Bedroom Shutter (ID: 10)
[DeviceType.THERMO_ZONE] 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:
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:
# 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():
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, 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, 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:
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 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:
# 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:
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.
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:
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:
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:
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:
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 redactedHost IP in the URL → last octet masked (e.g.
192.168.1.***)Usernames inside
sl_users_listitems → 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:
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.