Skip to content

eugeneniemand/EspHomeNet

Repository files navigation

EspHomeNet

A lightweight ESPHome API server written in .NET. It speaks the ESPHome plaintext TCP protocol so Home Assistant (or other ESPHome clients) can connect to a software "node", discover entities, and receive live state updates — no microcontroller required.

  • Target framework: .NET 9
  • Default port: 6053 (same as ESPHome firmware)
  • Status: prototype covering handshake, sensor discovery, state streaming, and a reconnect strategy for late-bound entities

Highlights

  • Handshake & Core Messages: Implements Hello, Authenticate, Device Info, Ping/Pong, Disconnect, ListEntities (sensors), SubscribeStates, and a basic SubscribeLogs responder.
  • Entity Registry: Register sensor entities at runtime, update values, and track subscribers so broadcasts only happen when clients are listening.
  • Dynamic Entity Discovery: When a brand-new entity is registered, the server now sends a DisconnectResponse and drops the socket so Home Assistant reconnects and reruns discovery automatically.
  • DI-Friendly Host: HostedEsphomeService listens for TCP clients, ConnectionHandler processes each connection, and the AddEsphomeServer() extension wires everything together.
  • Sample NetDaemon App: Demonstrates registering a sensor on-demand from a Home Assistant input_button press via NetDaemon.

Quickstart Guide

Follow these steps to run the server, connect Home Assistant via the ESPHome integration, and see live sensor updates.

1) Start the ESPHomeNet server

From the repository root, build and run the NetDaemon sample (it hosts the example publisher and the ESPHome server):

# from the repo root
dotnet restore
dotnet build
dotnet run --project EspHomeNet.NetDaemonSample

The server listens on port 6053 by default (see EspHomeNet/AppConfig.cs).

2) Add ESPHome integration in Home Assistant

  • In Home Assistant go to Settings → Devices & Services → Add Integration → ESPHome.
  • Enter the IP address of the machine running the server and port 6053.
  • The integration will show a node named ESPHomeNet. No entities appear until a publisher registers them.

3) Start updating a sensor (sample flow)

  • Ensure Home Assistant has a helper button (e.g. input_button.test_routine). Create it under Settings → Devices & Services → Helpers → Create helper → Button.
  • Press the helper button. The NetDaemon sample registers a sensor (e.g. sensor.netdaemon_button_counter) and forces the ESPHome connection to reconnect so Home Assistant rediscovers entities.
  • After discovery, pressing the button again will trigger the sample to publish a SensorStateResponse which updates the sensor value in Home Assistant in real time.

Troubleshooting tips:

  • If Home Assistant does not discover the node, verify the server process is running and reachable from the Home Assistant host (firewall, network).
  • Confirm port 6053 is used by the server and not blocked.
  • Check console logs from the sample app for handshake and register/disconnect events.

Repository Layout

  • EspHomeNet/ – library (server, registry, framing helpers, protobuf definitions, DI extensions)
  • EspHomeNet.NetDaemonSample/ – NetDaemon sample that bridges Home Assistant events to the ESPHome server
  • EspHomeNet.sln – solution file for Visual Studio or dotnet CLI

Prerequisites

  • .NET 9 SDK (or newer)
  • A Home Assistant instance if you want to run the NetDaemon sample (requires a long-lived access token and the input_button.test_routine helper)

Build

# From the repo root
dotnet restore
dotnet build

Run The NetDaemon Sample

  1. Update EspHomeNet.NetDaemonSample/appsettings.json with your Home Assistant host, port, SSL preference, and long-lived access token.

  2. Ensure Home Assistant has an input_button.test_routine entity (Settings → Devices & services → Helpers → + Create helper → Button).

  3. Start the sample:

    dotnet run --project EspHomeNet.NetDaemonSample
  4. In Home Assistant, add/refresh the ESPHome integration:

    • Host: machine running the sample
    • Port: 6053
    • The ESPHome integration will show a node named ESPHomeNet with no entities until the first button press.
  5. Press the input_button.test_routine. The NetDaemon app registers sensor.netdaemon_button_counter, triggers a forced disconnect, and Home Assistant immediately reconnects and discovers the new sensor. Subsequent presses call _server.UpdateEntityState(new SensorStateResponse { Key = 100, State = 1f }) to emit a state update.

Logs from the sample include NetDaemon connection status and ESPHome protocol messages so you can follow the handshake and reconnect flow.

Using The Library In Your App

using EspHomeNet;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

await Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddLogging(builder => builder.AddConsole());
        services.AddEsphomeServer();
        services.AddHostedService<MyPublisher>();
    })
    .RunConsoleAsync();

Key APIs:

  • IEsphomeServer.RegisterEntity(IMessage listEntityResponse) – register ESPHome entities by supplying their generated ListEntities*Response message.
  • IEsphomeServer.UpdateEntityState(IMessage stateResponse) – publish the matching generated *StateResponse to subscribed clients.
  • IEsphomeServer.EntityRegistered / EntityUpdated events – observe registry changes (the built-in server uses EntityRegistered to trigger client reconnects).

Configuration

  • Port is defined in EspHomeNet/AppConfig.cs (Port = 6053). Change this constant to listen on a different port.

Protocol Coverage (Current)

  • Handshake: HelloRequest (1) ↔ HelloResponse (2); AuthenticationRequest (3) ↔ AuthenticationResponse (4); DeviceInfoRequest (9) ↔ DeviceInfoResponse (10)
  • Connection Management: PingRequest (7) ↔ PingResponse (8); DisconnectRequest (5) ↔ DisconnectResponse (6)
  • Entities & States: ListEntitiesRequest (11) ↔ ListEntitiesSensorResponse (16) … ListEntitiesDoneResponse (19); SubscribeStatesRequest (20) → SensorStateResponse (25) broadcasts
  • Logs: SubscribeLogsRequest (28) → one initial SubscribeLogsResponse (29)

Notes:

  • Currently only sensor entities are implemented.
  • No command/control messages (e.g., lights, switches) are handled.
  • No Noise encryption; API encryption is reported as unsupported.

Internals

  • HostedEsphomeService hosts a TcpListener and spins up a DI-scoped ConnectionHandler for each client.
  • ConnectionHandler performs the handshake, replies to ESPHome requests, subscribes the connection, and now disconnects it when new entities appear so the client refreshes discovery.
  • EsphomeServer maintains the in-memory registry, publishes SensorStateResponse frames to subscribed clients, and exposes EntityRegistered / EntityUpdated events.
  • FramingHelpers implements ESPHome’s plaintext frame format (indicator byte + varints + protobuf payload).
  • Protobuf types live in EspHomeNet/Protos and are compiled via Grpc.Tools during the build.

Logging

  • Uses Microsoft.Extensions.Logging. Forced disconnects are logged at Debug level to avoid noisy expected errors.
  • The sample configures console logging for both NetDaemon and EspHomeNet namespaces; adjust filters to suit your environment.

Units & Statistics (Home Assistant)

Guidance for getting friendly units and statistics in Home Assistant. These are general HA/ESPHome conventions, along with what this library supports today.

  • Units
    • Use canonical symbols HA expects: e.g. °C, %, lx, W, kWh, ppm, µg/m³, m³, V, A, Hz.
    • Pair units with the correct device class in HA so it can validate/convert (temperature, energy, humidity, voltage, current, pressure, etc.).
    • In this library, populate the generated ListEntitiesSensorResponse (or other list response) fields that Home Assistant expects (for sensors: unit_of_measurement, accuracy_decimals, etc.).
    • Note: This library currently does not expose device_class or state_class fields yet; see “Statistics” below.

Example (ESPHome YAML for reference):

sensor:
  - platform: dallas
    address: 0x1C0000031EDD2A28
    name: "Water Temp"
    device_class: temperature
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    state_class: measurement
  • Statistics
    • Home Assistant records short/long-term stats only for sensors that opt-in via state_class and provide numeric values (with a unit when required).
    • Use one of:
      • state_class: measurement — instantaneous values (temperature, humidity, power, pressure, etc.).
      • state_class: total or total_increasing — monotonically increasing totals (energy, gas, water, impulses).
    • Ensure unit and device class match expectations (e.g., energy in kWh with device_class: energy + state_class: total_increasing).
    • Current status in this library: we do not yet emit device_class or state_class in ListEntitiesSensorResponse, so HA will not compute statistics for these sensors by default. This is a planned enhancement; until then you’ll see live values and history (recorder) but not HA’s derived statistics.

Examples (ESPHome YAML for reference):

Instantaneous (mean/min/max):

sensor:
  - platform: bme680
    temperature:
      name: "Living Room Temp"
      device_class: temperature
      unit_of_measurement: "°C"
      state_class: measurement

Total energy (sum):

sensor:
  - platform: sml
    energy_delivered:
      name: "Grid Energy"
      device_class: energy
      unit_of_measurement: "kWh"
      state_class: total_increasing
      accuracy_decimals: 3

If you currently only see categories/labels (e.g., a diagnostic entity or a text_sensor), switch to a numeric sensor entity with the appropriate device_class and state_class. Also ensure the recorder does not exclude it.

Here’s a detailed breakdown of device classes, units of measurement, and how they interact in Home Assistant / ESPHome. It’s quite a lot, but this is the “canonical” reference you can lean on when designing your sensors.


Overview: device_class, native_unit_of_measurement, unit_of_measurement

Before diving into the lists:

  • device_class: A semantic hint to Home Assistant about what kind of quantity a sensor represents (e.g. temperature, humidity, voltage, energy, etc.). It helps the frontend pick icons, supports unit conversions, and (in some cases) enforces which units are allowed.
  • native_unit_of_measurement (ESPHome / HA “native” unit): the unit in which the sensor actually reports its value internally.
  • unit_of_measurement: the (possibly converted) unit exposed to the user / UI. If Home Assistant’s unit system is different (e.g. °F vs °C), HA may convert between native_unit_of_measurement and unit_of_measurement.

For correct functioning, the combination of device_class and unit must align with what HA expects — otherwise you’ll see warnings/errors like “unit … is not valid for the device class …”

Also: not all device classes require or expect units (some are “unitless” by nature, like pH, or certain enumerations).


Device classes & valid units (and usage guidance)

Below is a list of common sensor device classes (from the HA dev docs) plus guidance about units and how to use them properly.

Device Class Valid / Expected Units Purpose / Notes / Tips
temperature °C, °F Use for temperature readings. HA will convert between units as needed.
humidity % Relative humidity (e.g. 0–100)
pressure / atmospheric_pressure Pa, hPa, bar, mbar, inHg, mmHg, psi, etc. Use for barometric or pressure sensors.
voltage V, mV For sensors reporting electrical potential.
current A, mA Electrical current.
power W, kW, mW Instantaneous power (e.g. load).
energy Wh, kWh, MWh, J, MJ, GJ For cumulative energy consumption (e.g. electricity meter). Must be consistent with expectations. Note: HA currently supports a limited set for energy (e.g. Wh, kWh, MJ, etc.). Using something like “kVAh” may generate warnings.
apparent_power VA, kVA, mVA Apparent power (real + reactive)
power_factor (unitless) Dimensionless ratio (0–1). No unit string is expected.
voltage V, mV (As above)
conductivity S/cm, mS/cm, µS/cm For electrical conductivity in fluids.
illuminance lx For light sensors
irradiance W/m², BTU/(h·ft²) Solar irradiance, etc.
co2 ppm CO₂ concentration
co ppm or mg/m³ Carbon monoxide concentration or mass density
ozone, nitrogen_dioxide, nitrogen_monoxide, nitrous_oxide µg/m³ Air pollutant concentrations
absolute_humidity g/m³, mg/m³ Water vapor absolute humidity
area m², cm², km², in², ft², etc. Physical area measurements
distance (or length) m, cm, mm, km, ft, in, yd, mi, nmi Spatial distances
duration (time) s, ms, min, h, d Time spans (not timestamps)
energy_distance Wh/km, kWh/100km, km/Wh, mi/kWh, etc. Energy per distance (useful in EV contexts)
energy_storage Wh, kWh, J, MJ, etc. Stored energy, e.g. in a battery
monetary ISO 4217 currency (e.g. USD, EUR) Monetary value (cost, price). Caveat: combining monetary with state_class: measurement may not be permitted or may trigger warnings in HA.
enum (none) — uses options list instead of numeric units For sensors representing discrete states (e.g. “low”, “medium”, “high”). Cannot be used with numeric state_class or native_unit_of_measurement.
pH (unitless) pH scale is inherently unitless
area (as above)
gas / volume / volume_storage L, m³, ft³, etc. For gas volume or fluid storage; note: device_class: gas is for actual gas consumption, not stored liquid propane. Some users instead prefer volume_storage for liquid storage.

Additionally, there are state classes that help HA know how to interpret sensor history/statistics:

  • state_class: measurement — for instantaneous values (temp, humidity, pressure)
  • state_class: total — for counters/accumulated totals that may increase or decrease
  • state_class: total_increasing — for monotonic counters (never go down, except reset cycles)
  • There is also a newer state_class: measurement_angle for angles (e.g. wind direction) in degrees.

When you set a device_class that implies numeric behavior, you must pair it with a proper unit and appropriate state_class so HA can record statistics.

Be especially careful when combining device_class and state_class — not all combinations are allowed or meaningful. For example, device_class: monetary expects state_class: total (not measurement) in HA’s current logic.


Examples & best practices

Here are a few example sensor definitions (ESPHome style) showing how to combine these:

sensor:
  - platform: dallas
    address: 0xabc
    name: "Outdoor Temperature"
    device_class: temperature
    unit_of_measurement: "°C"
    state_class: measurement
    accuracy_decimals: 1

  - platform: ina219
    current:
      name: "Load Current"
      device_class: current
      unit_of_measurement: "A"
      state_class: measurement
    power:
      name: "Load Power"
      device_class: power
      unit_of_measurement: "W"
      state_class: measurement
    energy:
      name: "Load Energy"
      device_class: energy
      unit_of_measurement: "Wh"
      state_class: total_increasing

  - platform: template
    name: "CO2 level"
    lambda: "return id(co2_sensor).state;"
    device_class: co2
    unit_of_measurement: "ppm"
    state_class: measurement
  • The energy sensor is cumulative, so state_class: total_increasing is appropriate because energy keeps increasing.

  • The temperature and current / power sensors are instantaneous and use state_class: measurement.

  • If you had a sensor that measures, say, the price per kWh, you might want:

    device_class: monetary
    unit_of_measurement: "USD"
    state_class: ???  # likely `measurement`, but HA may warn

If you saw a warning like “unit … not valid for device class (’temperature’)", it usually indicates a mismatch (e.g. using “°C” vs “⁰C”, or a bad unit string).

Limitations & Caveats

  • Protocol subset: sensor discovery/state updates only.
  • No TLS or Noise encryption; authentication always succeeds.
  • Intended for local development/testing. Behaviour may diverge from real ESPHome firmware.

Development Notes

  • Build: dotnet build
  • Run sample: dotnet run --project EspHomeNet.NetDaemonSample
  • Modify protobuf contracts under EspHomeNet/Protos; generated C# is refreshed automatically during build.

License

No license file is included. Add one before publishing or sharing the project.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages