Home Assistant Integration Patterns (Marstek)

SkillDev tools

Project-specific patterns for the Marstek integration (config flow, coordinator, scanner, entities, translations)

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Home Assistant Integration Patterns (Marstek) skill

What this skill tells your AI

The instructions your AI receives, as published by taurgis/has-marstek-local-api in .agents/skills/homeassistant-integration-patterns/SKILL.md and read by ahel’s review.

This skill helps you make correct, repo-consistent changes to this Home Assistant custom integration.

When to Use

  • Adding/changing sensors
  • Changing discovery/config flow behavior
  • Updating coordinator error handling
  • Working on translations or diagnostics
  • Ensuring changes meet Home Assistant integration quality expectations

Quick Map

TaskFile(s)
Setup / teardown / coordinator wiring__init__.py
Config flow (user, dhcp, integration discovery)config_flow.py
Central polling (tiered intervals)coordinator.py
IP-change scannerscanner.py
Sensorssensor.py (EntityDescription pattern)
Binary sensorsbinary_sensor.py (EntityDescription pattern)
Select entitiesselect.py
Servicesservices.py (idempotent registration)
Device automation actionsdevice_action.py
Device info helperdevice_info.py
Mode configurationmode_config.py
Diagnosticsdiagnostics.py
Text / translationsstrings.json, translations/en.json
Iconsicons.json
Local API referencedocs/marstek_device_openapi.MD
UDP client librarypymarstek/

Core Rules

  1. Coordinator-only I/O

    • Never add per-entity UDP calls.
    • Read everything from MarstekDataUpdateCoordinator.data.
    • Use _async_setup() for one-time initialization during first refresh.
    • Set always_update=False if data supports __eq__ comparison.
  2. Async-only

    • Only do async I/O; never block the event loop.

Coordinator Error Handling

async def _async_update_data(self):
    try:
        return await self.api.fetch_data()
    except AuthError as err:
        # Triggers reauth flow automatically
        raise ConfigEntryAuthFailed from err
    except RateLimitError:
        # Backoff with retry_after
        raise UpdateFailed(retry_after=60)
    except ConnectionError as err:
        raise UpdateFailed(f"Connection failed: {err}")
  1. Avoid unavailable clutter

    • Only create entities when there’s a corresponding data key in coordinator output.
    • Prefer explicit per-sensor classes or a description table keyed by coordinator data.
  2. Use translation-aware config-flow errors

    • Config flow errors should use keys defined in custom_components/marstek/strings.json.
    • Reauth flows should ask only for the changed credential and update the existing entry.
  3. Stable identifiers

    • Use BLE-MAC-based unique IDs for entities and devices; never pivot on IPs.
    • Keep _attr_has_entity_name = True and set device_info for grouping.

Adding a new sensor

Steps:

  1. Find the value on coordinator.data (a plain dict[str, Any] coming from pymarstek).
  2. Add a MarstekSensorEntityDescription to the SENSORS tuple in sensor.py.
  3. Use exists_fn to conditionally create entities (avoids permanent unavailable state).
  4. Keep the unique_id stable (BLE-MAC + sensor key).
  5. Add translation keys in translations/en.json (and keep strings.json in sync).
  6. Use suggested_display_precision for numeric sensors.
  7. Only register entities for data keys that exist to avoid permanent unavailable noise.

Entity Patterns (Mandatory for New Integrations)

class MarstekSensor(CoordinatorEntity, SensorEntity):
    _attr_has_entity_name = True  # MANDATORY

    def __init__(self, coordinator, description):
        super().__init__(coordinator)
        self.entity_description = description
        self._attr_unique_id = f"{coordinator.ble_mac}_{description.key}"
        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, coordinator.ble_mac)},
            name=coordinator.device_name,
            manufacturer="Marstek",
        )

EntityDescription Pattern (Recommended)

@dataclass(kw_only=True)
class MarstekSensorEntityDescription(SensorEntityDescription):
    value_fn: Callable[[dict], StateType]
    exists_fn: Callable[[dict], bool] = lambda _: True

SENSORS: tuple[MarstekSensorEntityDescription, ...] = (
    MarstekSensorEntityDescription(
        key="battery_soc",
        translation_key="battery_soc",
        device_class=SensorDeviceClass.BATTERY,
        native_unit_of_measurement=PERCENTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        value_fn=lambda data: data.get("soc"),
    ),
    MarstekSensorEntityDescription(
        key="power",
        device_class=SensorDeviceClass.POWER,
        native_unit_of_measurement=UnitOfPower.WATT,
        state_class=SensorStateClass.MEASUREMENT,
        value_fn=lambda data: data.get("power"),
    ),
)

Icon Translations (Preferred over icon property)

Create icons.json:

{
  "entity": {
    "sensor": {
      "battery_soc": {
        "default": "mdi:battery",
        "state": {
          "100": "mdi:battery",
          "50": "mdi:battery-50"
        }
      }
    }
  }
}

Entity Categories

  • EntityCategory.DIAGNOSTIC - RSSI, firmware version, temperature
  • EntityCategory.CONFIG - Settings the user can change
  • Set entity_registry_enabled_default = False for rarely-used sensors

State Classes for Energy Sensors

  • SensorStateClass.MEASUREMENT - Instantaneous values (power, temperature)
  • SensorStateClass.TOTAL - Values that can increase/decrease (net energy)
  • SensorStateClass.TOTAL_INCREASING - Only increases, resets to 0 (lifetime energy)
  • Use SensorDeviceClass.ENERGY_STORAGE for battery capacity (stored Wh)

Common pitfalls

  • Polling/discovery storms (too many UDP requests too frequently).
  • Doing IP discovery inside setup or coordinator updates (scanner already handles this).
  • Sending control commands without pausing polling (causes concurrent UDP traffic and flaky results).
  • Breaking unique IDs (must remain stable across upgrades and IP changes).
  • Skipping options reload listeners or reauth handling in config_flow.py.
  • Using aggressive polling intervals instead of event-driven triggers (prefer immediate scan on failure over shorter intervals).

Event-Driven Best Practices

When detecting connectivity issues or IP changes:

  1. Prefer event-driven over polling: Trigger immediate action on failure instead of shortening poll intervals
  2. Debounce: Add minimum intervals between event-triggered actions (e.g., 30s minimum between scans)
  3. Keep backup polling: Maintain a longer interval (10 min) as a backstop for edge cases

Example: The coordinator triggers MarstekScanner.async_request_scan() when it hits the failure threshold, enabling fast IP change detection without aggressive periodic scanning.

Signals

GitHub stars
35
Forks
5
Last commit
Sep 2026
Advanced
Item type
skill
Key
homeassistant-integration-patterns
Source
github.com/taurgis/has-marstek-local-api