This guide walks through the process of adding new entities to the integration -- whether extending an existing domain or creating a new one.
For background on why entities are organized the way they are, see the Architecture overview and the Entity Patterns reference.
Entities are organized by business domain (gateway, circuit, dhw, pool, etc.),
not by Home Assistant entity type. A single domain folder can contain sensors,
binary sensors, switches, and numbers side by side. Platform files at the
component root (sensor.py, binary_sensor.py, ...) act as orchestrators that
call builder functions from each domain.
This is the most common case. Suppose you need a new temperature sensor on the control unit.
The sensor reads from the main heat pump unit, so it belongs in
entities/control_unit/. If it were related to a heating circuit, it would go in
entities/circuit/; for domestic hot water, entities/dhw/; and so on.
For a sensor, open entities/control_unit/sensors.py. For a binary sensor, open
binary_sensors.py in the same directory. The naming convention is always the
plural HA platform name.
Find _build_control_unit_sensor_descriptions() and add a new
HitachiYutakiSensorEntityDescription entry. Here is the existing file with a
new discharge_temp sensor appended:
# In entities/control_unit/sensors.py
def _build_control_unit_sensor_descriptions() -> tuple[
HitachiYutakiSensorEntityDescription, ...
]:
"""Build control unit sensor descriptions."""
return (
# ... existing descriptions ...
HitachiYutakiSensorEntityDescription(
key="outdoor_temp",
translation_key="outdoor_temp",
description="Outdoor ambient temperature measurement",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda coordinator: coordinator.data.get("outdoor_temp"),
),
# --- new entry ---
HitachiYutakiSensorEntityDescription(
key="discharge_temp",
translation_key="discharge_temp",
description="Compressor discharge temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda coordinator: coordinator.data.get("discharge_temp"),
),
)The key must match a data key exposed by the coordinator in
coordinator.data. The translation_key is looked up in translation files.
Open translations/en.json and add an entry under the correct platform section:
{
"entity": {
"sensor": {
"discharge_temp": {
"name": "Discharge Temperature"
}
}
}
}en.json is the source of truth. Other languages are managed via Weblate or
direct JSON edits.
Some entities should only appear when the hardware supports them. Use the
condition field -- a lambda that receives the coordinator and returns a bool:
HitachiYutakiBinarySensorEntityDescription(
key="boiler",
translation_key="boiler",
description="Boiler running state",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda coordinator: coordinator.api_client.is_boiler_active,
condition=lambda c: c.profile.supports_boiler,
),The _create_* factory functions evaluate the condition at setup time and skip
entities whose condition returns False.
make test && make ha-runVerify the entity appears in the HA developer tools under the expected device.
When entities do not logically belong to any existing domain, create a new one.
entities/
new_domain/
__init__.py
sensors.py
entities/new_domain/sensors.py:
"""New domain sensor descriptions and builders."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.components.sensor import SensorDeviceClass
from ...const import DEVICE_CONTROL_UNIT
from ..base.sensor import (
HitachiYutakiSensor,
HitachiYutakiSensorEntityDescription,
_create_sensors,
)
if TYPE_CHECKING:
from ...coordinator import HitachiYutakiDataCoordinator
def build_new_domain_sensors(
coordinator: HitachiYutakiDataCoordinator,
entry_id: str,
) -> list[HitachiYutakiSensor]:
"""Build new domain sensor entities."""
descriptions = _build_new_domain_sensor_descriptions()
return _create_sensors(coordinator, entry_id, descriptions, DEVICE_CONTROL_UNIT)
def _build_new_domain_sensor_descriptions() -> tuple[
HitachiYutakiSensorEntityDescription, ...
]:
"""Build new domain sensor descriptions."""
return (
HitachiYutakiSensorEntityDescription(
key="my_register_key",
translation_key="my_register_key",
description="Human-readable purpose of this sensor",
device_class=SensorDeviceClass.TEMPERATURE,
value_fn=lambda coordinator: coordinator.data.get("my_register_key"),
),
)entities/new_domain/__init__.py:
"""New domain entities."""
from .sensors import build_new_domain_sensors
__all__ = [
"build_new_domain_sensors",
]In sensor.py, import the builder and call it inside async_setup_entry:
from .entities.new_domain import build_new_domain_sensors
async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
# ... existing builders ...
entities.extend(build_new_domain_sensors(coordinator, entry.entry_id))
async_add_entities(entities)Add keys to translations/en.json as described in step 4 above.
Dynamic device IDs instead of constants. Always use the constants from
const.py (DEVICE_CIRCUIT_1, DEVICE_CIRCUIT_2, etc.). Building device IDs
with string interpolation like f"circuit{circuit_id}" produces "circuit1"
instead of the correct "circuit_1", silently attaching entities to the wrong
device.
Business logic in entity classes. Entity descriptions should contain only
data access (coordinator.data.get(...)) and thin formatting. Any calculation,
accumulation, or decision logic belongs in domain/services/. The entity layer
must stay declarative.
Importing HA modules in the domain layer. The domain/ package must never
import from homeassistant.*. If your new entity needs complex logic, implement
it as a domain service with a Protocol interface and inject an adapter.
Missing conditions for optional features. If a sensor only makes sense when
a specific hardware feature is present (DHW, pool, cooling mode, second
compressor), add a condition lambda. Without it, entities appear for users
whose hardware does not support the feature and show permanently unavailable.
Using the wrong data key. If the value your entity reads seems stuck or
does not reflect the actual hardware state, the underlying register may be
wrong. See API Layer & Data Keys for the CONTROL vs STATUS
distinction -- this is handled in the API layer but affects which keys are
available in coordinator.data.
The entities/telemetry/ domain is different from other entity domains. Telemetry
entities don't read from coordinator.data (Modbus registers) — they read from
coordinator telemetry attributes (telemetry_collector, telemetry_last_send, etc.).
Because of this, the telemetry sensor uses a custom subclass
(HitachiYutakiTelemetrySensor) that overrides native_value and available
instead of relying on the standard value_fn pattern. See
entities/telemetry/sensors.py for the implementation.
Use this checklist before opening a PR:
- Entity is in the correct
entities/<domain>/directory - Description added to the
_build_*_descriptions()function keymatches a data key incoordinator.datatranslation_keyadded totranslations/en.jsonunder the right platformconditionlambda present for optional/hardware-dependent features- Device type uses a constant from
const.py(not a dynamic string) - No business logic in entity descriptions -- only data access
value_fnreads the correct data key (see API Layer & Data Keys if unsure)- Platform orchestrator updated if this is a new domain
make test && make lintpass