Skip to main content

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:

AttributeType / defaultMeaning
_attr_availablebool = TrueWhether the entity is reachable
_attr_assumed_statebool = FalseState is assumed, not read back
_attr_attributionstr | None = NoneData source attribution text
_attr_device_classstr | NoneDevice class (per platform enum)
_attr_device_infoDeviceInfo | None = NoneDevice registry metadata
_attr_entity_categoryEntityCategory | NoneCONFIG or DIAGNOSTIC
_attr_entity_picturestr | None = NonePicture URL
_attr_entity_registry_enabled_defaultboolEnabled when first registered
_attr_entity_registry_visible_defaultboolVisible when first registered
_attr_extra_state_attributesdict[str, Any]Additional state attributes
_attr_force_updateboolWrite every update, even if unchanged
_attr_has_entity_nameboolName describes only the entity (see naming)
_attr_iconstr | NoneIcon identifier
_attr_namestr | NoneEntity name
_attr_should_pollbool = TruePoll async_update for state
_attr_supported_featuresint | None = NoneFeature bitmask
_attr_translation_keystr | NoneKey into the integration's translations
_attr_translation_placeholdersMapping[str, str]Placeholders for translated names
_attr_unique_idstr | None = NoneStable unique ID (registry identity)
_attr_unit_of_measurementstr | NoneUnit (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:

  1. _attr_name, if set.
  2. If has_entity_name is true and translation_key is set: the translated name at component.<integration>.entity.<platform_domain>.<translation_key>.name (translation placeholders are substituted).
  3. entity_description.name, if an EntityDescription is used.
  4. 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, using self.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_state for customization).
  • schedule_update_ha_state(force_refresh=False) / async_schedule_update_ha_state(force_refresh=False) — schedule a state write, optionally polling async_update first.
  • With _attr_should_poll = True (the default) the platform polls your async_update method; push-based integrations set it to False and 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 — a SensorDeviceClass value; drives icons, formatting, and device-class-based naming.
  • _attr_state_class — a SensorStateClass value; 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" }
}
}