A Python coordinator library and breaker simulator for the Smart Breaker Local Communication Protocol (SBLCP), the local UDP protocol spoken by Eaton AbleEdge Smart Breaker 2.0 devices on the LAN. SBLCP is the AbleEdge instantiation of Eaton's EMLCP family (the same lineage as the first-generation EMCB and Eaton's emcb-udp-coordinator).
Status: early, working. The wire codec (framing, HMAC-SHA256 signing, sequence-number handling), the coordinator client, and a breaker simulator are implemented and tested against spec-derived vectors, including a coordinator↔simulator integration test over UDP loopback. Payload codecs cover every fixed message code: sequence sync, meter telemetry, device status, primary/remote handle read + write, trip log, energy reset, factory reset, production details, time sync, debug log + level, WiFi RSSI, identify-me, the Local-OTA HTTP server, UDP key management (TTL query + rotation), and
WHO_ARE_YOUbroadcast discovery (hash-based IP↔breaker identification viaCoordinator.who_are_you).
Rotating a key requires encrypting it under the device's secondary unicast key (AES-256-GCM); that lives in sblcp.crypto behind an optional dependency — pip install sblcp[crypto]. The core library is standard-library-only.
This library is deliberately standalone: it implements the SBLCP wire protocol and nothing more. It has no runtime dependencies beyond the Python standard library and no coupling to any particular application that consumes it, so it can back a coordinator daemon, a test harness, or a higher-level integration equally well.
- Transport: UDP on port 32866 over a 2.4 GHz WiFi LAN, IPv4 unicast and subnet broadcast.
- Model: coordinator → node. The coordinator (this library) always initiates; the breaker (node) validates and responds.
- Security: every message carries a 32-byte HMAC-SHA256 signature computed with a shared UDP key, plus a sequence number to defeat replays. Keys are 32 bytes, come in unicast and broadcast sets (each with a primary and secondary), and expire after 7 days.
- Framing: a
ETNM(coordinator) /ETNS(node) start marker, a little-endianu32sequence number, a big-endianu16message code, a little-endian message body, and the 32-byte hash footer. Minimum 42 bytes.
- In scope: the SBLCP fixed message codes (device status, meter telemetry, remote handle position, trip log, energy reset, time sync, RSSI, UDP-key management, sequence-number sync, ...), the coordinator transport, key management, node discovery (both the signed
WHO_ARE_YOUkind and the keyless ARP kind, seesblcp-arp), and a software simulator for hardware-free development and CI. - Out of scope (intentionally): a vendor extension envelope that carries a separate, non-public message model. This library implements the fixed message codes only; a raw payload for such a code can still be sent over the same transport via the low-level
request()API.
See doc/INTERFACE.md for the full Python API: the Coordinator client and its operations, the KeySet key model, the typed payloads and enums, the low-level request() escape hatch, and the BreakerNode simulator.
pip install -e ".[dev]"sblcp-sim # binds a fake breaker node on :32866SBLCP's own discovery signs every datagram, so Coordinator.discover() and who_are_you() are useless until a device has been provisioned. That is a real gap: a factory-fresh breaker is on your network and answering ARP, but nothing at the application layer.
sblcp-arp closes it by working purely from the host's ARP cache, which needs no keys and no root:
sblcp-arp # scan the local /24, list everything
sblcp-arp 192.168.1.0/24 --oui 2c:bc:bb # only devices from one vendor
sblcp-arp --no-populate --mac-suffix ab:cd:efnudging 192.168.1.0/24 ...
192.168.1.200 2c:bc:bb:ab:cd:ef on en0
Two filters, for two different situations:
| Filter | Use it when |
|---|---|
--mac-suffix ab:cd:ef |
You know part of one device's MAC and want its IP and full address. Reliable |
--oui 2c:bc:bb |
You want a rough sweep for one vendor prefix. Incomplete by nature, see below |
Prefer the suffix. A device that advertises over Bluetooth gives you only its low three octets, and a suffix match turns that into a full MAC and an IP without needing to know the vendor prefix at all.
An OUI filter finds only devices using the prefix you name, and a vendor may well use several. Two AbleEdge breakers observed on one network reported 2C:BC:BB and F0:24:F9: enumerating by either prefix alone would have found one and silently missed the other. Treat --oui as a starting point for exploration, never as a complete inventory.
By default the target network is nudged first: one empty UDP datagram is sent to every address, on the discard port. The kernel has to resolve each address to send, which populates the ARP cache as a side effect. Hosts ignore the datagram, as they should, but their network stack still answers the ARP.
That approach is deliberate. It needs no elevated privileges, crafts no raw frames, and shells out to nothing, so the package stays standard-library-only and works the same on macOS, BSD and Linux. Pass --no-populate to read the cache as it stands, which is faster and completely silent on the wire.
Reading the cache tries arp -an first and falls back to ip neigh, so hosts without net-tools are covered. Unresolved neighbours are skipped: an incomplete entry means something was asked about, not that anything answered.
from sblcp import find_hosts
# Resolve one device from the low-order octets in its BLE advertising name.
for entry in find_hosts("192.168.1.0/24", mac_suffix="ab:cd:ef"):
print(entry.ip, entry.mac, entry.interface)find_hosts() returns ArpEntry records sorted numerically by address. Also exported: read_arp_table(), populate_arp_cache(), and parse_arp_output() if you already have output to parse.
No OUI is built into this package. It implements a protocol, not a vendor's product, so pass the prefix you care about.
The version has a single source of truth: __version__ in src/sblcp/__init__.py. pyproject.toml reads it dynamically (dynamic = ["version"] plus [tool.setuptools.dynamic]), so there is exactly one line to bump. This repo is modern-build only: there is no setup.py shim and no second copy to keep in sync.
To cut a release:
- Bump
__version__insrc/sblcp/__init__.py(the only place). - Move the
CHANGELOG.md[Unreleased]entries into a new version section. - Commit (
git commit -am "release X.Y.Z"). - Tag
vX.Y.Zand push the tag. TheReleaseworkflow builds, checks the wheel carriesLICENSE, and publishes to PyPI by Trusted Publishing (OIDC), so no API token exists in this repository.
PyPI trusts exactly one workflow: owner electrification-bus, repository sblcp, workflow release.yml, environment pypi. The workflow name is a filename rather than the name: inside it, and the environment must match the publishing job's environment: key.
The workflow does not yet verify that the tag equals v$__version__. It should, per the version-single-source convention, so a tag can never disagree with the packaged version.
MIT © Clark Communications Corporation