| name | python-homeassistant |
| description | Architecture, entity patterns, testing, and conventions for the hass-iopool Home Assistant custom integration. |
| user-invocable | false |
Skill: Python — Home Assistant Integration (hass-iopool)
Use this skill for any implementation, review, or debugging task in the custom_components/iopool/ codebase.
1. Project Stack
- Language: Python 3.12+,
async/await everywhere for I/O
- Framework: Home Assistant custom component (no frontend, no database)
- API access: Direct HTTP calls via
aiohttp to https://api.iopool.com/v1 — no external wrapper library (requirements: [] in manifest.json)
- Documentation: iopool Public API — always fetch and read this page (including images) before implementing any API call. Context7 does not have iopool API knowledge.
- Linter:
flake8 --max-line-length=150
- Tests:
pytest with asyncio_mode = "auto" (see pyproject.toml)
2. Module Map
custom_components/iopool/
├── __init__.py # Entry setup/teardown, IopoolData wiring (filtration, coordinator)
├── api_models.py # IopoolAPIResponse, IopoolAPIResponsePool, IopoolLatestMeasure, IopoolAdvice
├── binary_sensor.py # BinarySensor platform
├── config_flow.py # UI config flow + options flow (CONFIG_VERSION=1)
├── const.py # All constants (DOMAIN, API endpoints, sensor types, config keys)
├── coordinator.py # IopoolDataUpdateCoordinator — polls every 300 s via aiohttp
├── diagnostics.py # HA diagnostics endpoint
├── entity.py # IopoolEntity base class (extends CoordinatorEntity + RestoreEntity)
├── filtration.py # Filtration logic and time-based scheduling
├── manifest.json # Integration metadata (no external requirements)
├── models.py # IopoolData, IopoolConfigData, IopoolConfigEntry alias
├── select.py # Select platform
├── sensor.py # Sensor platform
└── translations/ # en.json, fr.json — all entity human names
3. Key Patterns and Invariants
3.1 Typed Config Entry
Always use IopoolConfigEntry (defined in models.py) instead of raw ConfigEntry:
from .models import IopoolConfigEntry
IopoolConfigEntry = ConfigEntry[IopoolData]
3.2 Runtime Data Access
coordinator: IopoolDataUpdateCoordinator = entry.runtime_data.coordinator
config: IopoolConfigData = entry.runtime_data.config
filtration: Filtration = entry.runtime_data.filtration
3.3 Coordinator Data
coordinator.data
coordinator.data.pools
coordinator.get_pool_data(pool_id)
Always check for None before accessing pool data:
pool = coordinator.get_pool_data(pool_id)
if pool is None:
return
3.4 Entity Base Class
All entities extend IopoolEntity:
from .entity import IopoolEntity
class IopoolMySensor(IopoolEntity, SensorEntity):
pass
IopoolEntity already sets _attr_has_entity_name = True and provides device_info.
3.5 EntityDescription Pattern
@dataclass(frozen=True)
class IopoolSensorEntityDescription(SensorEntityDescription):
exists_fn: Callable[..., bool] = lambda _: True
3.6 unique_id Pattern
self._attr_unique_id = f"{config_entry_id}_{pool_id}_{description.key}"
3.7 Translation Keys
Every entity needs translation_key on the description. Add the key to both translations/en.json and translations/fr.json:
"entity": {
"sensor": {
"my_key": { "name": "Human Readable Name" }
}
}
3.8 API Error Handling
from aiohttp.client_exceptions import ClientError, ServerTimeoutError
try:
async with self.session.get(url, headers=self.headers) as response:
response.raise_for_status()
data = await response.json()
except (ClientError, ServerTimeoutError) as err:
raise UpdateFailed(f"Error communicating with iopool API: {err}") from err
3.9 Async Rules
- All I/O must use
async/await
- Use
@callback decorator on _handle_coordinator_update
- Never use blocking calls (no
time.sleep, no synchronous HTTP) in async context
- Use
_LOGGER = logging.getLogger(__name__) — never print()
4. Testing
See ../testing-hass-iopool/SKILL.md for the full project-specific test setup:
- Directory structure and test file map
- Tier 1 (pure constants, runs anywhere) vs Tier 2 (HA dependency, devcontainer only)
- Environment detection and
pytest run commands (devcontainer / docker exec)
- Shared fixtures from
conftest.py
- Test rules: what to test per entity, flake8 gate
For generic testing principles, see ../testing-qa/SKILL.md.
5. New Entity — Full Guide
5.1 Required Information (clarify before implementing)
If any of the following is unclear, gather the information via vscode_askQuestions before starting implementation.
- Entity type —
sensor, binary_sensor, or select
- Key — snake_case, unique within the platform (e.g.
pool_mode)
- What it represents — what data or state does it expose?
- Unit of measurement — if sensor (e.g.
mV, °C); omit for binary or select
- Conditional — does it require specific pool data to exist? If yes, define
exists_fn
5.2 Implementation Steps (all required, in one pass)
@dataclass(frozen=True) EntityDescription subclass with optional exists_fn
unique_id following the {entry_id}_{pool_id}_{key} pattern
translation_key added to both en.json and fr.json
- Row added to
docs/integration/entities.mdx
- Tests in
tests/test_<platform>.py — at minimum: unique_id format, entity state, exists_fn if defined
- Commit message:
feat(<platform>): ✨ Add <entity_name> entity
5.3 EntityDescription Pattern (full example)
@dataclass(frozen=True)
class IopoolSensorEntityDescription(SensorEntityDescription):
"""Sensor entity description for iopool."""
exists_fn: Callable[..., bool] = lambda _: True
SENSORS: tuple[IopoolSensorEntityDescription, ...] = (
IopoolSensorEntityDescription(
key="my_sensor",
translation_key="my_sensor",
icon="mdi:...",
native_unit_of_measurement="unit",
),
)
5.4 Entity Class and async_setup_entry
class IopoolMySensor(IopoolEntity, SensorEntity):
def __init__(
self,
coordinator: IopoolDataUpdateCoordinator,
description: IopoolSensorEntityDescription,
config_entry_id: str,
pool_id: str,
pool_name: str,
) -> None:
super().__init__(coordinator, config_entry_id, pool_id, pool_name)
self.entity_description = description
self._attr_unique_id = f"{config_entry_id}_{pool_id}_{description.key}"
async def async_setup_entry(
hass: HomeAssistant,
entry: IopoolConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
coordinator = entry.runtime_data.coordinator
pool_id: str = entry.data[CONF_POOL_ID]
pool = coordinator.get_pool_data(pool_id)
if pool is None:
return
async_add_entities(
IopoolMySensor(coordinator, description, entry.entry_id, pool.id, pool.title)
for description in SENSORS
if description.exists_fn(pool)
)
5.5 Coordinator Data
| Attribute | Type | Description |
|---|
coordinator.data | IopoolAPIResponse | Full API response |
coordinator.data.pools | list[IopoolAPIResponsePool] | All pools for the API key |
coordinator.get_pool_data(pool_id) | IopoolAPIResponsePool | None | Single pool by ID |
pool.latest_measure | IopoolLatestMeasure | None | Latest sensor measurement |
pool.advice | IopoolAdvice | None | iopool filtration advice |
5.6 Translation Format
Add under entity.<platform>.<translation_key> in both translations/en.json and translations/fr.json:
"entity": {
"sensor": {
"my_sensor": {
"name": "Human Readable Name"
}
}
}
5.7 Documentation Review on Every Code Change
Whenever a feature, service, entity, or behavior is added or modified, apply this process before considering the task complete:
-
Read the relevant docs/ pages — scan docs/integration/ to identify pages that cover the area being changed (entities, actions, events, webhook, setup, etc.).
-
Identify what needs documenting — new behavior, changed defaults, added options, new entities, removed capabilities, updated configuration steps, etc.
-
Preserve the existing style — match the tone, structure, table format, and MDX components already used on the target page. Do not introduce new formatting patterns.
-
When in doubt, ask the developer — if it is unclear whether a change warrants a documentation update, ask via vscode_askQuestions (mandatory — see global rule in copilot-instructions.md) before writing or skipping it:
"Should I update the documentation for this change? If yes, which page(s)?"
6. CI Checks (non-local)
| Check | Tool | What it validates |
|---|
hassfest | GitHub Actions | manifest.json, translations, quality_scale.yaml |
hacs/action | GitHub Actions | HACS compatibility |
CodeQL | GitHub Actions | Python security analysis |
7. Commit, PR, and Pre-commit Gate
See ../git-conventions/SKILL.md for the full rules:
- Commit message format (type, gitmoji, scope,
Tests: line)
- Pre-commit test gate — mandatory before every commit
- PR title, description template, branch rules, and prepare-PR workflow
8. HA Code Review Checklist
Use this checklist when reviewing any file in custom_components/iopool/.
8.1 Flake8 Compliance
- Max line length: 150 characters — flag any line exceeding this
- No unused imports, undefined names, or bare
except: clauses
- Run:
flake8 --max-line-length=150 <file>
8.2 Home Assistant Patterns
async/await for all I/O-bound operations (no blocking calls in async context)
_LOGGER = logging.getLogger(__name__) — no print() statements
CoordinatorEntity / IopoolEntity as base class for entities
IopoolConfigEntry used instead of raw ConfigEntry
entry.runtime_data.coordinator to access the coordinator
@callback decorator on _handle_coordinator_update
- Constants from
const.py — no magic strings or numbers inline
8.3 Entity Conventions (when applicable)
_attr_unique_id follows {entry_id}_{pool_id}_{key} pattern
_attr_has_entity_name = True is set (already in IopoolEntity)
translation_key set on the EntityDescription
exists_fn defined if the entity is conditional
8.4 Security
- No secrets or credentials logged
- API key is the only credential; never log or expose it
- No hardcoded URLs — use constants from
const.py
- No hardcoded tokens
8.5 Error Handling
- Exceptions caught at the appropriate level, logged with
_LOGGER.error or _LOGGER.warning
- Coordinator data access uses
.get() with safe fallbacks when a key may be absent
- No silent
except: pass blocks
8.6 Test Coverage Assessment
Report what is not yet tested in tests/test_<module>.py:
- Untested public methods or properties
- Untested error paths
- Missing edge-case coverage
8.7 Review Output Format
### Issues (must fix)
- `<file>:<line>` — description
### Suggestions (nice to have)
- description
### Test gaps
- description
9. Investigating HA Behavior Changes
Trigger: When the user mentions that a behavior changed since a specific HA version (e.g., "this worked in HA 2024.3 but broke in 2024.4"). Always perform this investigation before modifying integration code, to understand the root cause rather than patching symptoms.
Step 1 — Identify suspect HA files
Based on the integration code that changed behavior, identify the HA modules likely involved. Common locations:
homeassistant/components/<platform>/ — platform base classes and contracts
homeassistant/helpers/entity.py, entity_registry.py — entity lifecycle
homeassistant/components/alarm_control_panel/__init__.py — alarm panel contract
Step 2 — Analyze via Git (devcontainer)
The devcontainer contains a git clone of HA. Use it to inspect history directly.
Step 2a — Detect environment and locate the HA repo:
test -d /workspaces && echo "inside devcontainer" || echo "outside devcontainer"
find /workspaces -name ".git" -maxdepth 3 2>/dev/null
Step 2b — Browse commit history on a suspected file:
git -C <ha_repo_path> log --oneline --follow -- homeassistant/components/sensor/__init__.py
git -C <ha_repo_path> log --oneline v2024.3.0..v2024.4.0 -- <file>
git -C <ha_repo_path> show <commit_sha>
Outside devcontainer: retrieve the container ID first, then prefix with docker exec <CONTAINER_ID> (see ../testing-hass-iopool/SKILL.md §3).
Step 3 — Analyze via MCP GitHub
Use MCP GitHub tools to search history and context in home-assistant/core or home-assistant/frontend:
| Goal | Tool | Key parameters |
|---|
| Browse commits on a file | mcp_github_list_commits | owner=home-assistant, repo=core, path=homeassistant/components/... |
| Read a specific commit | mcp_github_get_commit | sha from the list above |
| Search code in HA | mcp_github_search_code | query=MyClass repo:home-assistant/core |
- Find related issues |
mcp_github_search_issues | query=breaking change sensor coordinator 2024.4 |
- Find related PRs |
mcp_github_search_pull_requests | query=sensor coordinator in home-assistant/core |
Primary repos to check (not exclusively):
home-assistant/core — Python backend, entities, platforms, helpers
home-assistant/frontend — UI components (if the change is visual/frontend)
Step 4 — Cross-reference with release notes
After finding relevant commits, confirm using HA release blog posts:
https://www.home-assistant.io/blog/ — release notes per version
- Look for Breaking changes or Deprecations sections mentioning the affected platform
Step 5 — Adapt integration code
Once the HA change is fully understood:
- Update affected files in
custom_components/iopool/
- Adjust tests that reflect the previous (now incorrect) behavior — see
../testing-hass-iopool/SKILL.md §7
- Run the test suite (see
../testing-hass-iopool/SKILL.md §3)
- Commit with scope and a
Tests: line (see ../git-conventions/SKILL.md)