ECHONET Lite library for Home Energy Management System (HEMS).
- ECHONET Lite frame encoding/decoding
- UDP multicast device discovery
- Async runtime client with event subscription
- Device state management with
DeviceManager - Adaptive poll scheduler for non-notifying properties via
PropertyPoller - Entity definitions based on MRA data
- Full type hints (
py.typed)
- Python 3.14+
- bidict>=0.23.0
MIT License
pip install pyhemsFor development setup and contribution guidelines, see CONTRIBUTING.md.
import asyncio
from pyhems import EOJ, HemsClient, HemsFrameEvent, HemsInstanceListEvent
async def main():
client = HemsClient(interface="0.0.0.0")
def on_event(event):
if isinstance(event, HemsInstanceListEvent):
print(f"Node: {event.node_id}, Instances: {event.instances}")
elif isinstance(event, HemsFrameEvent):
print(f"Frame from {event.node_id}: {event.frame}")
unsubscribe = client.subscribe(on_event)
await client.start()
# Read properties from a discovered device
# node_id = "fe..." # obtained from HemsInstanceListEvent
# props = await client.get(node_id, EOJ(0x013001), [0x80, 0xB3])
# Write a property and wait for Set_Res/SetC_SNA
# result = await client.set_property(node_id, EOJ(0x013001), 0x80, b"\x30")
await asyncio.sleep(60)
unsubscribe()
await client.stop()
asyncio.run(main())HemsClient.start()/HemsClient.stop(): Start and stop UDP transport. Starting also sends the initial discovery probe and starts recurring discovery after 30 seconds.HemsClient.subscribe(callback): Subscribe to runtime events.HemsClient.probe_initial_nodes(): Trigger the initial multicast node discovery using the identification number and self-node instance list.HemsClient.probe_nodes(): Trigger recurring multicast node discovery using only the self-node instance list.HemsClient.start_periodic_discovery(): Start recurring discovery immediately.HemsClient.get(node_id, deoj, epcs): Read property values. Returns onePropertyper requested EPC; values that could not be read are empty.HemsClient.request_get(node_id, deoj, epcs): Send one unsplit GET and return itsGetResponse, orNonewhen it is not answered.HemsClient.read_manufacturer_code(node_id, deoj): Read EPC 0x8A on its own once and remember it for the device object.HemsClient.set_property(node_id, deoj, epc, edt): Write a single property and wait for the acknowledged result.HemsClient.set_properties(node_id, deoj, properties): Write multiple properties and wait for the acknowledged result. The returnedSetRequestResultseparates accepted, rejected, and unanswered EPCs. This low-level client API does not update aDeviceManagercache.DeviceManager.set_properties(node_id, deoj, properties, *, refresh=False): Write through the runtime client and update cached values for accepted EPCs. Withrefresh=True, the accepted readable EPCs are read back shortly after the write so the cache reflects the value the device actually applied.DeviceManager.subscribe(callback): Receive typedDeviceManagerEvents:DeviceAdded,PropertiesChanged(with the changed EPCs),AvailabilityChanged, andRuntimeActivity. Property and availability events are emitted only when a value actually changes.DeviceManager.last_activity_at: Monotonic time of the latest frame received or device setup.DeviceManager.async_start()/DeviceManager.async_stop(): Subscribe to runtime events and process device events in arrival order. Setup of newly discovered devices runs in a separate task per node.
Requests to one node are sent one at a time through a per-node queue, ordered
by RequestPriority (Set, then setup and other reads, then polling); see
docs/adr/0011-serialize-requests-per-node.md.
get_codec() / get_codec_for_epc() provide typed scalar temporal
conversion: date becomes datetime.date, clock time
becomes naive datetime.time, duration time becomes datetime.timedelta,
and date-time becomes naive datetime.datetime. TemporalDefinition.kind
records the meaning; supported scalar MRA time properties are classified in
scripts/custom_definitions.yaml, not inferred from received values.
Supported widths are date=4, time=2/3 and date-time=6/7 bytes. Explicit MRA
sizes take precedence over defaults 4/3/7. Encoding rejects precision loss,
timezone-aware values, invalid calendars and out-of-range durations.
TemporalCodec.resolution exposes wire precision as a timedelta;
maximum_duration exposes the largest normal duration for duration codecs.
Callers can use these without interpreting byte widths or calculating wire limits.
Special oneOf states retain their MRA string keys; invalid values return
None. Like existing codecs, encoding does not enforce access rights; callers
must check the property's writable access. Scalar temporal values and simple
oneOfs containing only same-width explicit states are supported. A oneOf with
complex alternatives is excluded as a whole; this includes the washer/dryer
duration settings that combine normal times, states and relative levels.
EntityDefinition.temporal holds the normal value's meaning and size;
existing enum_values hold same-width special states. No recursive value tree
or state-specific access metadata is added.
Temporal conversion inside objects/arrays, MRA-external sentinels and automatic
clock synchronization are not added. Existing structured decoding is unchanged.
Node-profile temporal definitions are not included in the registry. See ADR-0013.
PropertyPoller uses an adaptive scheduler designed for mixed ECHONET Lite devices:
- One poll cycle per device at a time, each request awaited through the node's request queue, avoids request pileups on slow devices.
- Poll cadence is adjusted from latency EWMA and failure backoff, starting from the base interval.
- Instantaneous-value EPCs can be scheduled on a separate fast lane (
fast_poll_epcs). - Requests are split by the per-device batch limit learned during setup (see
docs/adr/); omitted EPCs are left to the next poll. - Poll targets can be narrowed dynamically through
DeviceManager.subscribe_epcs(...).
from pyhems import DeviceClass, REGISTRY
print(REGISTRY.version, REGISTRY.mra_version)
# Mapping: class_code -> tuple[EntityDefinition, ...]
ac_entities = REGISTRY.entities.get(DeviceClass.HOME_AIR_CONDITIONER, ())
for entity in ac_entities[:3]:
print(entity.epc, entity.name_en)DeviceClass is an IntEnum generated from the MRA shortName values. Custom
device classes defined in scripts/custom_definitions.yaml are included too.
Each EntityDefinition exposes readable/writable flags, enum keys (MRA
identifiers keep their MRA spelling, such as allStop; keys defined by pyhems,
such as true/false, are snake_case; see docs/adr/0012-keep-mra-identifiers.md),
its PropertyRole, and the value semantics derived by the generator from
the unit and the English name: quantity (physical quantity), value_kind
(measurement, total, or cumulative; None for static values such as ratings),
and indication (the condition indicated by a boolean true).
get_codec_for_epc(class_code, epc, *, manufacturer_code=None, byte_offset=0)
returns a codec converting between EDT bytes and Python values. Values whose
scale depends on sibling properties take those properties as context:
codec = get_codec_for_epc(0x0287, 0xC0)
value = codec.decode(node.properties[0xC0], node.properties)encode() raises ValueError for fields at a non-zero byte_offset and for
values outside minimum/maximum.