Entities
Entities are how integrations expose data and controls. Every entity derives from
the Entity base class in apexos/helpers/entity.py, usually indirectly through a
platform base class such as SensorEntity (apexos/components/sensor/__init__.py).
The _attr_ convention
Almost every entity property has a backing class attribute prefixed with _attr_.
Instead of overriding the property, set the attribute:
class MySensor(SensorEntity):
_attr_has_entity_name = True
_attr_native_unit_of_measurement = "°C"
def __init__(self, device_id: str) -> None:
self._attr_unique_id = f"{device_id}_temperature"
The entity metaclass wraps these in cached properties: writing to an _attr_
attribute automatically invalidates the corresponding cached property, so the next
property read reflects the new value.
Base Entity attributes
From apexos/helpers/entity.py:
| Attribute | Type / default | Meaning |
|---|---|---|
_attr_available | bool = True | Whether the entity is reachable |
_attr_assumed_state | bool = False | State is assumed, not read back |
_attr_attribution | str | None = None | Data source attribution text |
_attr_device_class | str | None | Device class (per platform enum) |
_attr_device_info | DeviceInfo | None = None | Device registry metadata |
_attr_entity_category | EntityCategory | None | CONFIG or DIAGNOSTIC |
_attr_entity_picture | str | None = None | Picture URL |
_attr_entity_registry_enabled_default | bool | Enabled when first registered |
_attr_entity_registry_visible_default | bool | Visible when first registered |
_attr_extra_state_attributes | dict[str, Any] | Additional state attributes |
_attr_force_update | bool | Write every update, even if unchanged |
_attr_has_entity_name | bool | Name describes only the entity (see naming) |
_attr_icon | str | None | Icon identifier |
_attr_name | str | None | Entity name |
_attr_should_poll | bool = True | Poll async_update for state |
_attr_supported_features | int | None = None | Feature bitmask |
_attr_translation_key | str | None | Key into the integration's translations |
_attr_translation_placeholders | Mapping[str, str] | Placeholders for translated names |
_attr_unique_id | str | None = None | Stable unique ID (registry identity) |
_attr_unit_of_measurement | str | None | Unit (platform-specific variants exist) |
Always set _attr_unique_id when you can derive a stable ID (serial number, MAC…):
it is what lets users rename, disable, and customize the entity.
Entity naming
Entity.name resolves in this order:
_attr_name, if set.- If
has_entity_nameis true andtranslation_keyis set: the translated name atcomponent.<integration>.entity.<platform_domain>.<translation_key>.name(translation placeholders are substituted). entity_description.name, if anEntityDescriptionis used.- A device-class-based name, for platforms that opt in.
With _attr_has_entity_name = True the name describes only the entity itself
(e.g. "Firmware"); the UI combines it with the device name. Example from
apexos/components/raspberry_pi/strings.json:
{
"entity": {
"update": {
"rpi_firmware": { "name": "Firmware" }
}
}
}
paired with _attr_translation_key = "rpi_firmware" on the entity.
Lifecycle and state writing
async_added_to_apex()— called when the entity is about to be added; subscribe to events here, usingself.async_on_remove(unsub)to auto-clean listeners.async_will_remove_from_apex()— called before removal.async_write_ha_state()— write the current properties to the state machine (must be called from the event loop; it is@final, override_async_write_ha_statefor customization).schedule_update_ha_state(force_refresh=False)/async_schedule_update_ha_state(force_refresh=False)— schedule a state write, optionally pollingasync_updatefirst.- With
_attr_should_poll = True(the default) the platform polls yourasync_updatemethod; push-based integrations set it toFalseand write state when data arrives.
A sensor example
SensorEntity adds sensor-specific attributes:
from apexos.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
class TemperatureSensor(SensorEntity):
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = "°C"
_attr_suggested_display_precision = 1
def __init__(self, coordinator, device_id: str) -> None:
self._attr_unique_id = f"{device_id}_temperature"
@property
def native_value(self):
return self._data.temperature
_attr_native_value/native_value— the sensor's value in its native unit (StateType | date | datetime | Decimal)._attr_native_unit_of_measurement— unit the value is measured in; the core handles user unit conversion._attr_device_class— aSensorDeviceClassvalue; drives icons, formatting, and device-class-based naming._attr_state_class— aSensorStateClassvalue; enables long-term statistics._attr_options— allowed values for enum sensors._attr_suggested_display_precision— decimal places suggested to the UI.
EntityCategory (from apexos.const) marks non-primary entities: CONFIG for
configuration entities, DIAGNOSTIC for read-only diagnostic entities.
Devices
Group entities onto a device by setting _attr_device_info (or passing
device_info to your entity):
from apexos.helpers.device_registry import DeviceInfo
device_info = DeviceInfo(
identifiers={(DOMAIN, board)},
manufacturer="Raspberry Pi",
model=MODELS.get(board),
name=BOARD_NAMES.get(board, "Raspberry Pi"),
)
Translations
User-visible texts live in the integration's strings.json — the source of
truth — with per-language files generated into translations/<lang>.json
(e.g. translations/en.json, translations/de.json). Sections include config
(config flow texts), entity (entity names by platform and translation key),
entity_component, exceptions, common, selector, triggers, and
conditions.
Reuse texts with key references:
"name": "[%key:component::sun::common::condition_threshold_name%]"
After editing strings.json, regenerate the English translation files:
python3 -m script.translations develop --integration <domain>
Exception messages can be translated too — raise with translation_domain and
translation_key and add the text under exceptions in strings.json:
{
"exceptions": {
"supervisor_not_ready": { "message": "Supervisor is not ready" }
}
}