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
- 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:
HostedEsphomeServicelistens for TCP clients,ConnectionHandlerprocesses each connection, and theAddEsphomeServer()extension wires everything together. - Sample NetDaemon App: Demonstrates registering a sensor on-demand from a Home Assistant
input_buttonpress via NetDaemon.
Follow these steps to run the server, connect Home Assistant via the ESPHome integration, and see live sensor updates.
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.NetDaemonSampleThe server listens on port 6053 by default (see EspHomeNet/AppConfig.cs).
- 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.
- 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
SensorStateResponsewhich 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
6053is used by the server and not blocked. - Check console logs from the sample app for handshake and register/disconnect events.
EspHomeNet/– library (server, registry, framing helpers, protobuf definitions, DI extensions)EspHomeNet.NetDaemonSample/– NetDaemon sample that bridges Home Assistant events to the ESPHome serverEspHomeNet.sln– solution file for Visual Studio ordotnetCLI
- .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_routinehelper)
# From the repo root
dotnet restore
dotnet build-
Update
EspHomeNet.NetDaemonSample/appsettings.jsonwith your Home Assistant host, port, SSL preference, and long-lived access token. -
Ensure Home Assistant has an
input_button.test_routineentity (Settings → Devices & services → Helpers → + Create helper → Button). -
Start the sample:
dotnet run --project EspHomeNet.NetDaemonSample
-
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.
-
Press the
input_button.test_routine. The NetDaemon app registerssensor.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 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 generatedListEntities*Responsemessage.IEsphomeServer.UpdateEntityState(IMessage stateResponse)– publish the matching generated*StateResponseto subscribed clients.IEsphomeServer.EntityRegistered/EntityUpdatedevents – observe registry changes (the built-in server usesEntityRegisteredto trigger client reconnects).
- Port is defined in
EspHomeNet/AppConfig.cs(Port = 6053). Change this constant to listen on a different port.
- 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.
HostedEsphomeServicehosts aTcpListenerand spins up a DI-scopedConnectionHandlerfor each client.ConnectionHandlerperforms the handshake, replies to ESPHome requests, subscribes the connection, and now disconnects it when new entities appear so the client refreshes discovery.EsphomeServermaintains the in-memory registry, publishesSensorStateResponseframes to subscribed clients, and exposesEntityRegistered/EntityUpdatedevents.FramingHelpersimplements ESPHome’s plaintext frame format (indicator byte + varints + protobuf payload).- Protobuf types live in
EspHomeNet/Protosand are compiled viaGrpc.Toolsduring the build.
- Uses
Microsoft.Extensions.Logging. Forced disconnects are logged atDebuglevel to avoid noisy expected errors. - The sample configures console logging for both NetDaemon and EspHomeNet namespaces; adjust filters to suit your environment.
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_classorstate_classfields 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_classand provide numeric values (with a unit when required). - Use one of:
state_class: measurement— instantaneous values (temperature, humidity, power, pressure, etc.).state_class: totalortotal_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_classorstate_classinListEntitiesSensorResponse, 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.
- Home Assistant records short/long-term stats only for sensors that opt-in via
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.
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_measurementandunit_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).
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 decreasestate_class: total_increasing— for monotonic counters (never go down, except reset cycles)- There is also a newer
state_class: measurement_anglefor 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.
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_increasingis 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).
- 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.
- Build:
dotnet build - Run sample:
dotnet run --project EspHomeNet.NetDaemonSample - Modify protobuf contracts under
EspHomeNet/Protos; generated C# is refreshed automatically during build.
No license file is included. Add one before publishing or sharing the project.