Metadata-Version: 2.4
Name: ekm-proxy
Version: 0.1.0
Summary: Proxy EKM Metering devices onto the Electrification Bus (eBus / Homie 5)
Author: Clark Communications Corporation
License-Expression: MIT
Keywords: ekm,omnimeter,iostack,metering,sub-metering,ebus,homie,proxy
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: ebus-sdk>=0.11.0
Requires-Dist: paho-mqtt>=2.0
Requires-Dist: requests>=2.28
Requires-Dist: tomli>=2.0; python_version < "3.11"
Provides-Extra: rs485
Requires-Dist: pyserial>=3.5; extra == "rs485"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# ekm-proxy

[![PyPI](https://img.shields.io/pypi/v/ekm-proxy.svg)](https://pypi.org/project/ekm-proxy/)
[![Test](https://github.com/electrification-bus/ekm-proxy/actions/workflows/test.yml/badge.svg)](https://github.com/electrification-bus/ekm-proxy/actions/workflows/test.yml)
[![Lint](https://github.com/electrification-bus/ekm-proxy/actions/workflows/lint.yml/badge.svg)](https://github.com/electrification-bus/ekm-proxy/actions/workflows/lint.yml)
[![spec drift](https://github.com/electrification-bus/ekm-proxy/actions/workflows/spec-drift.yml/badge.svg)](https://github.com/electrification-bus/ekm-proxy/actions/workflows/spec-drift.yml)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)

Publishes **EKM Metering** devices (Omnimeter meters, and ioStack IO/sensor modules) onto the **Electrification Bus (eBus)** as Homie 5 devices.

It is an eBus **proxier** (see the eBus [`proxy.md`](https://github.com/electrification-bus/specification/blob/main/data-models/proxy.md) pattern): EKM meters do not speak eBus natively, so this service reads their data and republishes each discrete device onto eBus/Homie 5. It is not an "adapter" in the vague sense; it is a proxy with a specific, first-class role in the eBus model.

> Status: **scaffold / MVP.** The cloud-MQTT source is the first target; other sources are stubbed with a common interface (see Variants).

## What it emits

- Each **Omnimeter** → one `energy.ebus.device.circuit` device bearing the eBus `meter` (and `demand`) capability plus `info`. Behind-the-meter sub-metering is modeled as a capability on a host circuit, not a standalone device type; per-conductor legs are `-a`/`-b`/`-c` property suffixes on the one device.
- Each **ioStack** → an IO/sensor device. Its non-electrical sensors (e.g. 1-Wire temperature) are **deferred** pending the eBus `homie/5` domain-partitioning decision (spec issue SPEC-611) and are not emitted yet.
- Device IDs follow the proxier convention `{proxier-id}-{proxied-id}` (the meter/ioStack serial is the proxied id).

Full context, the field-mapping rationale, and the "why MVP-first is safe" spec-dependency analysis are maintained in the eBus specification project's design notes.

## Architecture: shared core + pluggable sources

All variants share the same **core** (EKM→eBus field mapping + Homie 5 emitter) and differ only in the **source** (how EKM data is acquired). Ports-and-adapters:

```
 source (acquire EKM data)  ->  core.mapping (EKM fields -> eBus props)  ->  core.emitter (Homie 5 -> eBus MQTT)
```

- `ekm_proxy/mapping.py` — the EKM-field → eBus-property mapping (source-agnostic).
- `ekm_proxy/emitter.py` — Homie 5 device/description + value publisher to the eBus broker.
- `ekm_proxy/sources/` — pluggable data sources; each yields normalized `readMeter`-style readings.

## Variants (sources)

Recall there will be several deployment variants of this proxy; they are all just different `sources/` behind the same core:

| Source | Path | Cloud? | Status | Notes |
|---|---|---|---|---|
| **cloud-mqtt** (MVP) | `sources/cloud_mqtt.py` | yes | done (MVP) | Subscribe the EKM Push3 Home Assistant MQTT discovery stream (`homeassistant/device/+/config` + `ekmdata/+/state`). Curated field subset (only what the operator exposed as HA entities). |
| **rs485** | `sources/rs485.py` | no | planned (full-detail path) | Talk RS-485 directly to Omnimeters (via EKM's `ekmmeters.py`). Full field detail, fully offline, and no Push3 required (just a low-cost USB-RS485 adapter). |
| **cloud-api** | `sources/cloud_api.py` | yes | TBD if needed | Poll the EKM cloud `readMeter` API (full field set). See the note below on why this is deferred. |
| **local-api** | `sources/local_api.py` | no | TBD if needed | Poll a Push3's onboard LAN `readMeter` API (`http://<push3-ip>/readMeter?...`), fully offline. Requires a Push3. See the note below. |
| **push3-native** | (separate) | no | future | Run *on* the Push3 itself. Platform/language TBD; likely not this Python package. Placeholder documented in `sources/push3_native.py`. |

`cloud-mqtt` and the `readMeter`-based sources (`cloud-api`, `local-api`) share this Python package. The two `readMeter` variants are **TBD if needed**: their advantages over `cloud-mqtt` are full field fidelity and offline (LAN) operation, but `rs485` already delivers both without a Push3 (just a low-cost USB-RS485 adapter), so it is the preferred path to full detail. The `readMeter` sources earn their place mainly where a Push3 is already deployed and you want to reuse its aggregation without wiring to the RS-485 bus. `push3-native` is a separate consideration because it depends on the Push3's on-device runtime.

## eBus spec status

This proxy composes existing eBus capabilities (`info` / `meter` / `demand`) under the `proxy.md` pattern. It emits `energy.ebus.device.circuit` bearing the `meter` capability, conforming to the eBus specification's capability-on-host model for behind-the-meter sub-metering (`data-models/circuit.md`, `capabilities/meter.md`). Instrument nameplate (`vendor-name` / `serial-number` / `model` / `firmware-version`) rides on the circuit's `info` per `capabilities/info.md` §"Nameplate versus conductor identity", with the EKM serial mirrored into `info/external-ids` as `ekm:<serial>`.

## How to run

Step-by-step for the MVP `cloud_mqtt` variant. Anyone with MQTT broker credentials can run it; no EKM hardware is required. It subscribes to the Home Assistant MQTT discovery stream an EKM Push3 publishes, translates each meter, and republishes it as an eBus (Homie 5) device.

### 1. What you need

- Python 3.10 or newer, with `pip` (a virtual environment is recommended).
- The Mosquitto client tools (`mosquitto_sub` / `mosquitto_pub`) for watching the output. Optional, but handy for step 5.
- Access to two MQTT brokers, which may be the **same** broker:
  - a **source** broker carrying an EKM Push3's Home Assistant discovery topics (`homeassistant/device/+/config`, `ekmdata/+/state`, `ekmdata/+/availability`). This is any broker an EKM Push3 publishes to (your own Push3, or a shared/demo broker). You supply the MQTT credentials.
  - a **target** eBus broker to publish the translated Homie 5 devices to. Can be the same broker, or a separate/local one. To stand up a local eBus broker on your own machine, see [broker-quickstart](https://github.com/electrification-bus/broker-quickstart).

### 2. Install

ekm-proxy is published on [PyPI](https://pypi.org/project/ekm-proxy/), so the quickest install is:

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install ekm-proxy
```

That installs the `ekm-proxy` command plus its dependencies: the eBus library `ebus-sdk` and `paho-mqtt`. For the RS-485 source, install the extra with `pip install "ekm-proxy[rs485]"`.

To hack on the proxy itself, install from a source checkout instead:

```bash
git clone https://github.com/electrification-bus/ekm-proxy.git
cd ekm-proxy
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
```

If your Python package index does not carry `ebus-sdk`, install it from the `electrification-bus` GitHub organization.

### 3. Configure

```bash
cp config/config.example.toml config/config.toml
```

Edit `config/config.toml`. The two sections you set are the **source** (`[source.cloud_mqtt]`, the broker with the EKM discovery stream) and the **target** (`[ebus]`, where translated devices are published):

```toml
[source]
kind = "cloud_mqtt"

[source.cloud_mqtt]
host = "${EKM_MQTT_HOST}"       # broker carrying the Push3 HA discovery stream
port = 1883
username = "${EKM_MQTT_USER}"   # password comes from EKM_MQTT_PASS

[ebus]
host = "${EBUS_MQTT_HOST}"      # where translated eBus devices are published
port = 1883
username = "${EBUS_MQTT_USER}"  # password comes from EBUS_MQTT_PASS
proxier_id = "ekm-proxy-1"      # your bridge id; devices are {proxier-id}-{serial}
vendor_name = "ekm-proxy"
name = "EKM Proxy"
```

Provide the credentials via environment variables (never commit secrets):

```bash
# source broker (the EKM Push3 / demo broker):
export EKM_MQTT_HOST=<source-broker-host>
export EKM_MQTT_USER=<source-username>
export EKM_MQTT_PASS=<source-password>

# target eBus broker (may be the same host):
export EBUS_MQTT_HOST=<target-broker-host>
export EBUS_MQTT_USER=<target-username>
export EBUS_MQTT_PASS=<target-password>
```

To publish back to the same broker you read from (the simplest setup), point the `EBUS_MQTT_*` values at the same host and credentials as the `EKM_MQTT_*` ones.

### 4. Run

```bash
ekm-proxy --config config/config.toml
```

It connects, discovers each EKM meter from the retained HA discovery messages, and publishes one eBus device per meter. Leave it running; it updates property values as new readings arrive.

### 5. Verify

Watch the eBus tree on the **target** broker:

```bash
mosquitto_sub -h <target-broker-host> -p 1883 -u <user> -P <pass> -t 'ebus/5/#' -v
```

You should see:

- `ebus/5/<proxier-id>/...` : the bridge (`energy.ebus.device.bridge`) root device.
- `ebus/5/<proxier-id>-<serial>/...` : one circuit (`energy.ebus.device.circuit`) per EKM meter, each with a typed `$description`, a `$state`, an `info` capability (nameplate: vendor, serial, plus `external-ids = ekm:<serial>`), and a `meter` capability (`imported-energy`, `active-power`, `current-a`, ...) in eBus-canonical units (energy in Wh, power in W, current in A).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for how to file issues, propose changes, and the lint/test expectations (`ruff` + `pytest`).

## Releasing

Published to [PyPI](https://pypi.org/project/ekm-proxy/) via Trusted Publishing (GitHub Actions OIDC, no stored token): the [`publish.yml`](.github/workflows/publish.yml) workflow builds and uploads on any `v*` tag. To cut a release:

1. Bump the version in **both** `pyproject.toml` (`[project].version`) and `src/ekm_proxy/__init__.py` (`__version__`); the `setup.py` shim reads the latter for the Yocto/kirkstone build path, so they must agree. Update the `CHANGELOG.md` `[Unreleased]` section.
2. Commit, then tag and push:
   ```bash
   git tag v0.1.0 && git push origin v0.1.0
   ```
3. The workflow runs the tests, builds the sdist + wheel, and publishes. Confirm the new version on PyPI and that the badge updates.

The `setup.py` shim exists so the older setuptools pinned in Yocto kirkstone builds a correct wheel (all modules plus the `ekm-proxy` entry point) from the sdist; the modern build path uses the PEP 621 metadata in `pyproject.toml`.

## License

[MIT](LICENSE) (c) 2026 Clark Communications Corporation.

## References (EKM public docs)

- EKM Push Open API / MQTT / RS-485: <https://documents.ekmmetering.com/api-docs/>
- Push3 LAN UI + local `readMeter` API: <https://help.ekmmetering.com/support/solutions/articles/6000236439>
- Omnimeter Pulse v.4, ioStack, Push3: <https://www.ekmmetering.com/>

Captured copies of these docs and live data captures are kept private (EKM's docs are copyrighted and are not redistributed here).
