# Ha Entity Platforms

> Create Home Assistant entity platforms (sensor, binary_sensor, switch, light, cover, climate, etc.). Use when the user asks to create entities, add a sensor platform, implement a switch, or work with any Home Assistant entity type. Also triggers on mentions of "SensorEntity", "SwitchEntity", "entity platform", "native_value", "device_class", or specific platform names. Use when this capability is needed.

- Skill: `tomevault-io/ha-entity-platforms` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/ha-entity-platforms`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/ha-entity-platforms/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/ha-entity-platforms

---


# Home Assistant Entity Platforms

Entity platforms are the files that define what entities your integration exposes (sensors, switches, lights, etc.). Each platform file follows a consistent pattern.

## Platform Setup Pattern

Every platform file implements `async_setup_entry`:

```python
"""Sensor platform for {Name}."""
from __future__ import annotations

from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import {Name}ConfigEntry
from .coordinator import {Name}Coordinator


async def async_setup_entry(
    hass: HomeAssistant,
    entry: {Name}ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up sensors from a config entry."""
    coordinator = entry.runtime_data

    entities = []
    for device_id in coordinator.get_device_ids():
        for description in SENSOR_DESCRIPTIONS:
            entities.append({Name}Sensor(coordinator, description, device_id))

    async_add_entities(entities)
```

## Entity Description Pattern (Recommended)

Use `EntityDescription` dataclasses for clean, declarative entity definitions:

```python
from dataclasses import dataclass
from collections.abc import Callable
from typing import Any

from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfTemperature


@dataclass(frozen=True, kw_only=True)
class {Name}SensorDescription(SensorEntityDescription):
    """Describe a {Name} sensor."""
    value_fn: Callable[[dict[str, Any]], Any]


SENSOR_DESCRIPTIONS: tuple[{Name}SensorDescription, ...] = (
    {Name}SensorDescription(
        key="temperature",
        translation_key="temperature",
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        value_fn=lambda data: data.get("temperature"),
    ),
    {Name}SensorDescription(
        key="humidity",
        translation_key="humidity",
        device_class=SensorDeviceClass.HUMIDITY,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=PERCENTAGE,
        value_fn=lambda data: data.get("humidity"),
    ),
    {Name}SensorDescription(
        key="battery",
        translation_key="battery",
        device_class=SensorDeviceClass.BATTERY,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=PERCENTAGE,
        value_fn=lambda data: data.get("battery_level"),
    ),
)
```

## Base Entity Class

Create a shared base class to avoid duplication across platforms:

```python
"""Base entity for {Name}."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import {Name}Coordinator


class {Name}Entity(CoordinatorEntity[{Name}Coordinator]):
    """Base class for {Name} entities."""

    _attr_has_entity_name = True

    def __init__(self, coordinator: {Name}Coordinator, device_id: str) -> None:
        """Initialize."""
        super().__init__(coordinator)
        self._device_id = device_id

    @property
    def device_info(self) -> DeviceInfo:
        """Return device info to group entities under one device."""
        device = self.coordinator.get_device_data(self._device_id)
        return DeviceInfo(
            identifiers={(DOMAIN, self._device_id)},
            name=device.get("name", self._device_id) if device else self._device_id,
            manufacturer="Manufacturer",
            model=device.get("model") if device else None,
            sw_version=device.get("firmware") if device else None,
        )

    @property
    def available(self) -> bool:
        """Available when coordinator is connected and device data exists."""
        return super().available and self.coordinator.get_device_data(self._device_id) is not None
```

## Sensor Entity

```python
class {Name}Sensor({Name}Entity, SensorEntity):
    """Sensor entity."""

    entity_description: {Name}SensorDescription

    def __init__(
        self,
        coordinator: {Name}Coordinator,
        description: {Name}SensorDescription,
        device_id: str,
    ) -> None:
        super().__init__(coordinator, device_id)
        self.entity_description = description
        self._attr_unique_id = f"{DOMAIN}_{device_id}_{description.key}"

    @property
    def native_value(self) -> float | str | None:
        device_data = self.coordinator.get_device_data(self._device_id)
        if device_data is None:
            return None
        return self.entity_description.value_fn(device_data)
```

## Binary Sensor Entity

```python
from homeassistant.components.binary_sensor import (
    BinarySensorDeviceClass,
    BinarySensorEntity,
    BinarySensorEntityDescription,
)

BINARY_SENSOR_DESCRIPTIONS = (
    BinarySensorEntityDescription(
        key="motion",
        translation_key="motion",
        device_class=BinarySensorDeviceClass.MOTION,
    ),
    BinarySensorEntityDescription(
        key="door",
        translation_key="door",
        device_class=BinarySensorDeviceClass.DOOR,
    ),
)

class {Name}BinarySensor({Name}Entity, BinarySensorEntity):
    """Binary sensor entity."""

    def __init__(self, coordinator, description, device_id):
        super().__init__(coordinator, device_id)
        self.entity_description = description
        self._attr_unique_id = f"{DOMAIN}_{device_id}_{description.key}"

    @property
    def is_on(self) -> bool | None:
        device_data = self.coordinator.get_device_data(self._device_id)
        if device_data is None:
            return None
        return device_data.get(self.entity_description.key)
```

## Switch Entity

Switches require control methods. Request a coordinator refresh after commands:

```python
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity

class {Name}Switch({Name}Entity, SwitchEntity):
    """Switch entity."""

    _attr_device_class = SwitchDeviceClass.SWITCH

    def __init__(self, coordinator, key, device_id):
        super().__init__(coordinator, device_id)
        self._key = key
        self._attr_unique_id = f"{DOMAIN}_{device_id}_{key}"
        self._attr_translation_key = key

    @property
    def is_on(self) -> bool | None:
        device_data = self.coordinator.get_device_data(self._device_id)
        if device_data is None:
            return None
        return device_data.get(self._key)

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the switch on."""
        await self.coordinator.client.async_set_switch(self._device_id, self._key, True)
        await self.coordinator.async_request_refresh()

    async def async_turn_off(self, **kwargs) -> None:
        """Turn the switch off."""
        await self.coordinator.client.async_set_switch(self._device_id, self._key, False)
        await self.coordinator.async_request_refresh()
```

## Entity Platform Quick Reference

| Platform | Base Class | Key Property | Device Classes |
|---|---|---|---|
| `sensor` | `SensorEntity` | `native_value` | TEMPERATURE, HUMIDITY, POWER, ENERGY, BATTERY, ... |
| `binary_sensor` | `BinarySensorEntity` | `is_on` | MOTION, DOOR, WINDOW, SMOKE, MOISTURE, ... |
| `switch` | `SwitchEntity` | `is_on` + `turn_on/off` | SWITCH, OUTLET |
| `light` | `LightEntity` | `is_on` + `brightness` | — |
| `cover` | `CoverEntity` | `is_closed` + `open/close/stop` | BLIND, GARAGE, SHUTTER, ... |
| `climate` | `ClimateEntity` | `hvac_mode` + `temperature` | — |
| `button` | `ButtonEntity` | `async_press()` | RESTART, UPDATE, IDENTIFY |
| `number` | `NumberEntity` | `native_value` + `set_value` | — |
| `select` | `SelectEntity` | `current_option` + `select_option` | — |
| `lock` | `LockEntity` | `is_locked` + `lock/unlock` | — |
| `update` | `UpdateEntity` | `installed_version` | — |

## Additional Resources

For a complete reference of all device classes, state classes, and binary sensor device classes, see [reference/device-classes.md](reference/device-classes.md).

## Critical Rules

1. Always set `_attr_has_entity_name = True` — entity names are derived from device name + entity name
2. Always provide `unique_id` — must be stable across restarts
3. Always provide `device_info` — groups entities under a device in the UI
4. Use `translation_key` for entity names, not hardcoded `_attr_name`
5. Add `state_class` to numeric sensors (MEASUREMENT, TOTAL, TOTAL_INCREASING) for statistics
6. Use `native_unit_of_measurement` instead of `unit_of_measurement`
7. Call `async_request_refresh()` after commands to get updated state

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/l3digitalnet) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-13 -->

