The Comwatt Python Client is a Python library that provides a convenient way to interact with the Comwatt API. It allows you to authenticate users, retrieve authenticated user information, and access site and device data.
Please note that the Comwatt client is exclusively for gen4 devices: it use energy.comwatt.com/api. Versions below gen4 will not be compatible. If you're looking for the go.comwatt.com/api go to python-comwatt-client-legacy.
The client currently supports the following methods:
authenticate(self, username, password): Authenticates a user with the provided username and password. The client re-authenticates automatically on session expiry (HTTP 401) and retries the request once; passComwattClient(auto_reauth=False)to disable this.is_authenticated(self): Returns whether the current session is still valid (probes the API;True/False, re-raises unexpected errors).logout(self): Logs the current session out server-side (POST /v1/logout) and clears the stored credentials soauto_reauthcannot silently restore the session. Idempotent (a 401 when already logged out is a no-op). Note this is distinct fromclose(), which only releases the local HTTP session without logging out.get_authenticated_user(self): Retrieves information about the authenticated user.get_sites(self): Retrieves a list of sites associated with the authenticated user.get_site_networks_ts_time_ago(self, site_id, measure_kind = "FLOW", aggregation_level = "NONE", aggregation_type = None, time_ago_unit = "HOUR", time_ago_value = 1, start = None, end = None): Retrieves the time series data for the networks of a specific site, based on the provided parameters. Deprecated — useget_site_time_seriesinstead (emits aDeprecationWarning; the endpoint still works but the app has moved tosite-time-series).get_site_consumption_breakdown_time_ago(self, site_id, aggregation_level = "HOUR", time_ago_unit = "DAY", time_ago_value = 1, start = None, end = None)Retrieves the consumption breakdown data for a specific site, based on the provided parameters. Deprecated — useget_top_consumptioninstead (emits aDeprecationWarning; the endpoint still works but the app has moved totop-consumption).get_devices(self, site_id): Retrieves a list of devices for the specified site.get_connected_objects(self, site_id=None, gateway_uid=None): Retrieves the connected objects for a site or a gateway. Exactly one ofsite_id/gateway_uidis required (raisesValueErrorotherwise).get_connected_object(self, connected_object_id): Retrieves information about a specific connected object.get_measure_keys(self, site_id): Retrieves the measure keys (flat measurement inventory) for a site — each a(device, measureKind)pair with a stable id andmeasureKeyUUID.get_tiles(self, site_id): Retrieves the dashboard tile configuration for a site (tile type + which device each points at; configuration only, no live values).get_electricity_price(self, site_id): Retrieves the EDF Tempo calendar / tariff structure for a site (tempoSyntheses,daily, ...).get_device_kinds(self, site_uid): Retrieves the device-kind catalogue for a site (the "add a device" picker). Takes the shortsiteUidstring (fromsite["siteUid"]), not the numeric site id.get_device_ts_time_ago(self, device_id, measure_kind = "FLOW", aggregation_level = "HOUR", aggregation_type = "MAX", time_ago_unit = "DAY", time_ago_value = "1", start = None, end = None): Retrieves the time series data for a specific device, based on the provided parameters.get_site_time_series(self, site_id, measure_kind = "FLOW", aggregation_level = "HOUR", aggregation_type = None, time_ago_unit = "DAY", time_ago_value = 1, start = None, end = None): Retrieves the whole-site rollup time series data for a specific site, based on the provided parameters.get_top_consumption(self, site_id, aggregation_level = "DAY", time_ago_unit = "DAY", time_ago_value = 1, start = None, end = None): Retrieves the per-device consumption breakdown (top 5 devices + "others") for a specific site.get_ecowatt(self): Retrieves the RTE EcoWatt grid-stress forecast (array of daily entries with a GREEN/ORANGE/RED status and 24 hourly values). Takes no parameters.switch_capacity(self, capacity_id, enable): Turns a switch/relay capacity on or off (PUT /capacities/{id}/switch). Takes the numeric capacityid.set_pilot_wire(self, capacity_id, state): Sets the pilot-wire order of a heating capacity (PUT /capacities/{id}/pilot-wire).stateis passed through as-is; valid values are backend-defined (check the capacity'sselectValues) and were not verified against a live pilot-wire device.set_thermal_mode(self, capacity_id, state): Sets the thermal mode of a thermostat capacity, e.g. eco / comfort (PUT /capacities/{id}/thermal-mode). Samestatepass-through caveat asset_pilot_wire.set_thermostat_set_point(self, capacity_id, value): Sets the target set-point of a thermostat capacity (PUT /capacities/{id}/thermostat-set-point).value(temperature) is passed through as-is.
start/end accept a datetime or ISO-8601 string and select an absolute window instead of the relative time_ago_* params; a naive datetime is treated as UTC:
from datetime import datetime
client.get_device_ts_time_ago(
"device-1",
start=datetime(2026, 7, 4, 0, 0, 0),
end=datetime(2026, 7, 5, 0, 0, 0),
)get_device(self, device_id): Retrieves information about a specific device.put_device(self, device_id, payload): Updates a specific device with the provided payload.stream_measurements(self, site, *, reconnect=False, reconnect_backoff=1.0, reconnect_backoff_max=60.0, reconnect_max_attempts=None): Streams live measurements (FLOW/STATE) for a single site over STOMP-over-WebSocket. Takes one site dict (as returned byget_sites); yieldsMeasurement/CapacityChangedevents. Requires the optional[stream]extra (websocket-client). Passreconnect=Trueto enable opt-in exponential-backoff reconnection on socket drops;reconnect_backoffsets the initial delay (seconds),reconnect_backoff_maxcaps the delay, andreconnect_max_attemptslimits consecutive failed connect attempts before raising (None= unlimited). Reconnect is off by default; an auth rejection is always terminal.
You can install the Comwatt Python Client using pip. Run the following command:
pip install comwatt-client
Here's a simple example of how to use the Comwatt Python Client:
from comwatt_client import ComwattClient
# Create a Comwatt client instance
client = ComwattClient()
# Authenticate the user
client.authenticate('username', 'password')
# Get information about the authenticated user
user_info = client.get_authenticated_user()
print(user_info)
# Get a list of sites associated with the authenticated user
sites = client.get_sites()
print(sites)
# Get the whole-site rollup time series (productions, consumptions, injections, ...)
site_time_series_data = client.get_site_time_series(sites[0]['id'])
print(site_time_series_data)
# Get the per-device consumption breakdown for a specific site (top 5 + "others")
top_consumption_data = client.get_top_consumption(sites[0]['id'])
print(top_consumption_data)
# Get a list of devices for a specific site
devices = client.get_devices(sites[0]['id'])
print(devices)
# Get time series data for a specific device
time_series_data = client.get_device_ts_time_ago(devices[0]['id'])
print(time_series_data)
# Set the control mode of a specific device to MANUL
device = client.get_device(devices[0]['id'])
device['configuration']['controlMode'] = 'MANUAL'
client.put_device(device['id'], device)
# Switch the POWER_SWITCH capacity
for feature in device['features']:
for capacity in feature['capacities']:
if capacity.get('capacity', {}).get('nature') == "POWER_SWITCH":
capacity_id = capacity['capacity']['id']
client.switch_capacity(capacity_id, False)
client.switch_capacity(capacity_id, True)Make sure to replace 'username', 'password' with the actual values for your Comwatt account.
All errors raised by the client subclass ComwattError:
ComwattAuthError— the credentials were rejected (HTTP 401/403 on login) or the session expired (HTTP 401). Re-authenticate.ComwattAPIError— any other unexpected HTTP status, including non-credential failures ofauthenticate()(e.g. a 5xx during an outage, or a 200 response missing the session cookie). Exposes.status_code,.url,.detail.
from comwatt_client import ComwattClient, ComwattAuthError, ComwattAPIError
client = ComwattClient()
try:
client.authenticate("username", "password")
sites = client.get_sites()
except ComwattAuthError:
... # credentials wrong or session expired — re-authenticate
except ComwattAPIError as e:
print(e.status_code, e.url, e.detail)For live measurements, install the optional streaming extra (adds websocket-client):
pip install comwatt-client[stream]
Then iterate a single site's measurements as they arrive over STOMP-over-WebSocket:
from comwatt_client import ComwattClient
client = ComwattClient()
client.authenticate("username", "password")
sites = client.get_sites()
for ev in client.stream_measurements(sites[0], reconnect=True):
print(ev)The generator yields Measurement (live FLOW / STATE values) and CapacityChanged (switch/state changes) events. By default (reconnect=False) it stops when the WebSocket drops. Pass reconnect=True to reconnect automatically with full-jitter exponential backoff:
for ev in client.stream_measurements(
sites[0],
reconnect=True,
reconnect_backoff=1.0,
reconnect_backoff_max=60.0,
reconnect_max_attempts=10,
):
print(ev)The delay between failed connection attempts starts at reconnect_backoff seconds and doubles up to reconnect_backoff_max. reconnect_max_attempts counts consecutive failed attempts (resets to zero after any successful connection). An auth rejection (HTTP 401/403) is always terminal and will not be retried. QUANTITY measures are emitted only on bucket rollover, so keep a slow REST fallback (e.g. get_site_time_series) for absolute totals.
The stream yields measurements keyed by an opaque capacityId string. Sensor and switch capacities use AZUREIOT-co.{N}.instances.{i}.{sensor|switch}.{j}.data; grid-exchange and battery capacities keep the sensor.{j} segment and append the quantity instead — ...sensor.{j}.withdrawal.data, ...sensor.{j}.injection.data, ...sensor.{j}.battery_charge.data, ...sensor.{j}.battery_discharge.data. To route them to a device and interpret them, join them against get_connected_objects(site_id):
-
capacityId↔ capacity object —Measurement.capacity_idis thecapacityIdstring found on each capacity object. That same object also carriesdeviceId,nature(e.g.CLAMP,POWER_SENSOR,POWER_SWITCH),measureKind, andproduction(bool) — enough to route a measurement to its device and determine its sign with no separate map. -
Multi-instance / polyphase devices — a device can expose several
SENSORcapacities (a 3-phase solar inverter pushesinstances.0,instances.1,instances.2). The device's instantaneous power is the sum of itsFLOWcapacities, not any single instance. Polyphase capacities carry aphasefield such asPOSITIVE_1/POSITIVE_2/POSITIVE_3(not every capacity has one). -
Typed value fields —
Measurementparses the raw string intovalue_float(when it is numeric — watts forFLOW) andvalue_bool(when it istrue/false— on/off forSTATE). In practiceFLOWpopulatesvalue_floatandSTATEpopulatesvalue_bool;Measurement.valueis the raw string the server sent — prefer the typed field. -
Cadence — the server pushes measurements in bursts roughly every 15 seconds, grouped per connected object (the
co.{N}segment of thecapacityId). It is not a fixed per-device poll: expect a burst of many measures close together, then a gap. -
Use
get_connected_objects(), notget_devices(), to build the route —get_connected_objects(site_id)returns each connected object (theco.{N}segment, via itscoId) with all its capacities, each carryingcapacityId/deviceId/nature/measureKind.get_devices()returns the user-facing device list, but the grid-meter and battery parents come back with emptyfeaturesandcapacities, so their routable capacities are not exposed there.This is the only way to map grid exchange and battery:
...sensor.{j}.withdrawal.dataroutes to aWITHDRAWALpart-child device,...sensor.{j}.injection.datato anINJECTIONone,...sensor.{j}.battery_charge.datato aBATTERY_CHARGEdevice, and...sensor.{j}.battery_discharge.datato aBATTERY_DISCHARGEdevice. These are real devices — part-children of theGRID_METER/BATTERYparent (identified bydeviceKind.code, not anaturefield) — that also show up inget_measure_keys(site_id). Some physical clamps on a multi-channel box are present as capacities but havedeviceId: nulluntil they are assigned to a device — drop those from the route. -
Switch state — a device with a
POWER_SWITCH/RELAYcapacity receives its on/off state on the stream asSTATEmeasurements on a...switch.N.datacapacityId, parallel to the...sensor.N.dataFLOWcapacityIdon the same instance. -
CapacityChangedvs the route —CapacityChangedis also yielded for switch state changes, but itscapacity_idis the numeric capacityid(the oneswitch_capacitytakes), not thecapacityIdstring — don't match it against the string route. Switch changes were not observed during this check (no switch was toggled manually).
Building the capacityId → (deviceId, nature, production) route from get_connected_objects() (covers every mappable capacity, including grid exchange and battery):
from comwatt_client import ComwattClient, Measurement
client = ComwattClient()
client.authenticate("username", "password")
sites = client.get_sites()
route = {}
for obj in client.get_connected_objects(site_id=sites[0]["id"]):
for cap in obj["capacities"]:
device_id = cap["deviceId"]
if device_id is None:
continue
route[cap["capacityId"]] = (device_id, cap["nature"], cap.get("production", False))
for ev in client.stream_measurements(sites[0], reconnect=True):
if isinstance(ev, Measurement):
device_id, nature, production = route.get(ev.capacity_id, (None, None, None))
if device_id is not None:
print(f"device={device_id} nature={nature} production={production} "
f"value={ev.value_float if ev.value_float is not None else ev.value_bool}")Contributions to the Comwatt Python Client are welcome! If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the GitHub repository.