Skip to content

Commit bef890f

Browse files
committed
chore: hacs validation, tests, lint
Signed-off-by: Damian Skrzyński <polprog.tech@gmail.com>
1 parent 217961f commit bef890f

30 files changed

Lines changed: 254 additions & 313 deletions

.github/workflows/ci.yml

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,23 @@ on:
66
pull_request:
77
branches: [main]
88

9-
concurrency:
10-
group: ${{ github.workflow }}-${{ github.ref }}
11-
cancel-in-progress: true
12-
139
jobs:
1410
lint:
15-
name: Lint & Format
11+
name: Lint
1612
runs-on: ubuntu-latest
1713
steps:
1814
- uses: actions/checkout@v4
1915
- uses: actions/setup-python@v5
2016
with:
2117
python-version: "3.12"
22-
- name: Install dependencies
23-
run: |
24-
pip install ruff
25-
- name: Ruff check
18+
- name: Install ruff
19+
run: pip install ruff
20+
- name: Run ruff check
2621
run: ruff check .
27-
- name: Ruff format check
22+
- name: Run ruff format check
2823
run: ruff format --check .
2924

30-
typecheck:
25+
type-check:
3126
name: Type Check
3227
runs-on: ubuntu-latest
3328
steps:
@@ -37,37 +32,23 @@ jobs:
3732
python-version: "3.12"
3833
- name: Install dependencies
3934
run: |
40-
pip install mypy homeassistant
41-
- name: Mypy
35+
pip install mypy
36+
pip install homeassistant
37+
pip install -r requirements_test.txt
38+
- name: Run mypy
4239
run: mypy custom_components/entitymap --ignore-missing-imports
40+
continue-on-error: true
4341

4442
test:
45-
name: Tests
43+
name: Test
4644
runs-on: ubuntu-latest
47-
strategy:
48-
matrix:
49-
python-version: ["3.12", "3.13"]
5045
steps:
5146
- uses: actions/checkout@v4
5247
- uses: actions/setup-python@v5
5348
with:
54-
python-version: ${{ matrix.python-version }}
49+
python-version: "3.12"
5550
- name: Install dependencies
5651
run: |
57-
pip install pytest pytest-asyncio pytest-cov homeassistant voluptuous aiohttp
52+
pip install -r requirements_test.txt
5853
- name: Run tests
59-
run: |
60-
pytest tests/ -v --cov=custom_components/entitymap --cov-report=term-missing
61-
- name: Check coverage
62-
run: |
63-
pytest tests/ --cov=custom_components/entitymap --cov-fail-under=70
64-
65-
hacs:
66-
name: HACS Validation
67-
runs-on: ubuntu-latest
68-
steps:
69-
- uses: actions/checkout@v4
70-
- name: HACS Validation
71-
uses: hacs/action@main
72-
with:
73-
category: integration
54+
run: pytest tests/ -v --cov=custom_components/entitymap --cov-report=term-missing

.github/workflows/validate.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: HACS Validation
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
- cron: "0 0 * * 0"
10+
11+
jobs:
12+
validate-hacs:
13+
name: HACS Validation
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- name: HACS Action
18+
uses: hacs/action@main
19+
with:
20+
category: integration
21+
22+
validate-hassfest:
23+
name: Hassfest Validation
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Hassfest
28+
uses: home-assistant/actions/hassfest@master

custom_components/entitymap/__init__.py

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
DOMAIN,
2424
PANEL_ICON,
2525
PANEL_TITLE,
26-
PANEL_URL,
2726
)
2827
from .graph import GraphBuilder
2928

@@ -45,9 +44,7 @@ class EntityMapRuntimeData:
4544
type EntityMapConfigEntry = ConfigEntry[EntityMapRuntimeData]
4645

4746

48-
async def async_setup_entry(
49-
hass: HomeAssistant, entry: EntityMapConfigEntry
50-
) -> bool:
47+
async def async_setup_entry(hass: HomeAssistant, entry: EntityMapConfigEntry) -> bool:
5148
"""Set up EntityMap from a config entry."""
5249
from .panel import EntityMapPanelView
5350
from .services import async_register_services
@@ -87,6 +84,7 @@ async def async_setup_entry(
8784
# Schedule initial scan
8885
options = entry.options
8986
if options.get(CONF_SCAN_ON_STARTUP, DEFAULT_SCAN_ON_STARTUP):
87+
9088
async def _startup_scan(_event: Event) -> None:
9189
"""Run initial scan after HA is fully started."""
9290
await builder.async_build()
@@ -99,6 +97,7 @@ async def _startup_scan(_event: Event) -> None:
9997

10098
# Auto-refresh on registry changes
10199
if options.get(CONF_AUTO_REFRESH, DEFAULT_AUTO_REFRESH):
100+
102101
@callback
103102
def _handle_registry_change(_event: Event) -> None:
104103
"""Schedule a rescan when registries change."""
@@ -108,14 +107,10 @@ def _handle_registry_change(_event: Event) -> None:
108107
"entity_registry_updated",
109108
"device_registry_updated",
110109
):
111-
unsub_listeners.append(
112-
hass.bus.async_listen(event_type, _handle_registry_change)
113-
)
110+
unsub_listeners.append(hass.bus.async_listen(event_type, _handle_registry_change))
114111

115112
# Periodic reconciliation scan
116-
scan_interval = options.get(
117-
CONF_SCAN_INTERVAL_HOURS, DEFAULT_SCAN_INTERVAL_HOURS
118-
)
113+
scan_interval = options.get(CONF_SCAN_INTERVAL_HOURS, DEFAULT_SCAN_INTERVAL_HOURS)
119114

120115
async def _periodic_scan(_now: Any) -> None:
121116
"""Periodic reconciliation scan."""
@@ -135,16 +130,12 @@ async def _periodic_scan(_now: Any) -> None:
135130
return True
136131

137132

138-
async def _async_update_options(
139-
hass: HomeAssistant, entry: EntityMapConfigEntry
140-
) -> None:
133+
async def _async_update_options(hass: HomeAssistant, entry: EntityMapConfigEntry) -> None:
141134
"""Handle options update — reload the integration."""
142135
await hass.config_entries.async_reload(entry.entry_id)
143136

144137

145-
async def async_unload_entry(
146-
hass: HomeAssistant, entry: EntityMapConfigEntry
147-
) -> bool:
138+
async def async_unload_entry(hass: HomeAssistant, entry: EntityMapConfigEntry) -> bool:
148139
"""Unload a config entry."""
149140
from homeassistant.components import frontend
150141

@@ -189,9 +180,7 @@ async def _async_register_panel(hass: HomeAssistant) -> None:
189180
)
190181

191182

192-
async def _async_create_repair_issues(
193-
hass: HomeAssistant, builder: GraphBuilder
194-
) -> None:
183+
async def _async_create_repair_issues(hass: HomeAssistant, builder: GraphBuilder) -> None:
195184
"""Create repair issues for critical fragility findings."""
196185
from homeassistant.helpers import issue_registry as ir
197186

@@ -202,9 +191,7 @@ async def _async_create_repair_issues(
202191

203192
# Count device_id references
204193
device_id_count = sum(
205-
1
206-
for f in findings
207-
if f.fragility_type == FragilityType.DEVICE_ID_REFERENCE
194+
1 for f in findings if f.fragility_type == FragilityType.DEVICE_ID_REFERENCE
208195
)
209196
if device_id_count > 0:
210197
ir.async_create_issue(
@@ -218,11 +205,7 @@ async def _async_create_repair_issues(
218205
)
219206

220207
# Missing entity references
221-
missing_refs = [
222-
f
223-
for f in findings
224-
if f.fragility_type == FragilityType.MISSING_ENTITY
225-
]
208+
missing_refs = [f for f in findings if f.fragility_type == FragilityType.MISSING_ENTITY]
226209
for finding in missing_refs[:5]: # Limit to 5 repair issues
227210
related = finding.related_node_ids[0] if finding.related_node_ids else "unknown"
228211
ir.async_create_issue(
@@ -256,9 +239,7 @@ def _register_websocket_commands(hass: HomeAssistant) -> None:
256239
from .analysis import analyze_impact
257240
from .fragility import detect_fragility
258241

259-
@websocket_api.websocket_command(
260-
{vol.Required("type"): "entitymap/graph"}
261-
)
242+
@websocket_api.websocket_command({vol.Required("type"): "entitymap/graph"})
262243
@websocket_api.async_response
263244
async def ws_get_graph(
264245
hass: HomeAssistant,
@@ -318,9 +299,7 @@ async def ws_get_neighborhood(
318299
{"nodes": nodes, "edges": [e.as_dict() for e in edges]},
319300
)
320301

321-
@websocket_api.websocket_command(
322-
{vol.Required("type"): "entitymap/scan"}
323-
)
302+
@websocket_api.websocket_command({vol.Required("type"): "entitymap/scan"})
324303
@websocket_api.async_response
325304
async def ws_scan(
326305
hass: HomeAssistant,
@@ -338,9 +317,7 @@ async def ws_scan(
338317
{"node_count": graph.node_count, "edge_count": graph.edge_count},
339318
)
340319

341-
@websocket_api.websocket_command(
342-
{vol.Required("type"): "entitymap/findings"}
343-
)
320+
@websocket_api.websocket_command({vol.Required("type"): "entitymap/findings"})
344321
@websocket_api.async_response
345322
async def ws_get_findings(
346323
hass: HomeAssistant,
@@ -378,17 +355,13 @@ async def ws_get_migration(
378355
if not builder:
379356
connection.send_error(msg["id"], "not_loaded", "EntityMap not loaded")
380357
return
381-
suggestions = get_migration_report(
382-
builder.graph, msg["node_id"], msg.get("target_node_id")
383-
)
358+
suggestions = get_migration_report(builder.graph, msg["node_id"], msg.get("target_node_id"))
384359
connection.send_result(
385360
msg["id"],
386361
{"suggestions": [s.as_dict() for s in suggestions]},
387362
)
388363

389-
@websocket_api.websocket_command(
390-
{vol.Required("type"): "entitymap/hierarchy"}
391-
)
364+
@websocket_api.websocket_command({vol.Required("type"): "entitymap/hierarchy"})
392365
@websocket_api.async_response
393366
async def ws_get_hierarchy(
394367
hass: HomeAssistant,
@@ -418,7 +391,8 @@ def _get_builder(hass: HomeAssistant) -> GraphBuilder | None:
418391
return None
419392
entry = entries[0]
420393
if hasattr(entry, "runtime_data") and entry.runtime_data:
421-
return entry.runtime_data.builder
394+
builder: GraphBuilder = entry.runtime_data.builder
395+
return builder
422396
return None
423397

424398

@@ -502,8 +476,10 @@ def _build_hierarchy(graph: Any) -> dict[str, Any]:
502476
area["entities"] = entities_by_area.get(area_id, [])
503477
# Counts
504478
area["device_count"] = len(area["devices"])
505-
area["entity_count"] = area["device_count"] + len(area["entities"]) + sum(
506-
len(d["entities"]) for d in area["devices"]
479+
area["entity_count"] = (
480+
area["device_count"]
481+
+ len(area["entities"])
482+
+ sum(len(d["entities"]) for d in area["devices"])
507483
)
508484
result_areas.append(area)
509485

custom_components/entitymap/adapters/automation.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414

1515
_LOGGER = logging.getLogger(__name__)
1616

17-
ENTITY_ID_PATTERN = re.compile(
18-
r"\b([a-z_]+\.[a-z0-9_]+)\b"
19-
)
17+
ENTITY_ID_PATTERN = re.compile(r"\b([a-z_]+\.[a-z0-9_]+)\b")
2018

2119

2220
class AutomationAdapter(SourceAdapter):
@@ -72,9 +70,7 @@ def _process_automation(
7270
self._process_trigger(graph, auto_entity_id, trigger)
7371

7472
# Conditions
75-
for condition in _as_list(
76-
config.get("condition", config.get("conditions", []))
77-
):
73+
for condition in _as_list(config.get("condition", config.get("conditions", []))):
7874
self._process_condition(graph, auto_entity_id, condition)
7975

8076
# Actions
@@ -302,9 +298,7 @@ def _process_action(
302298
self._process_action(graph, auto_entity_id, sub)
303299

304300
@staticmethod
305-
def _ensure_placeholder(
306-
graph: DependencyGraph, node_id: str, node_type: NodeType
307-
) -> None:
301+
def _ensure_placeholder(graph: DependencyGraph, node_id: str, node_type: NodeType) -> None:
308302
"""Ensure a node exists in the graph (placeholder if not from registry)."""
309303
if node_id not in graph.nodes:
310304
graph.add_node(

custom_components/entitymap/adapters/group.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
from __future__ import annotations
44

55
import logging
6-
from typing import Any
7-
8-
from homeassistant.core import HomeAssistant
96

107
from ..const import Confidence, DependencyKind, NodeType
118
from ..models import DependencyGraph, GraphEdge, GraphNode
@@ -30,9 +27,7 @@ async def async_populate(self, graph: DependencyGraph) -> None:
3027
GraphNode(
3128
node_id=group_entity_id,
3229
node_type=NodeType.GROUP,
33-
title=state.attributes.get(
34-
"friendly_name", group_entity_id
35-
),
30+
title=state.attributes.get("friendly_name", group_entity_id),
3631
entity_id=group_entity_id,
3732
)
3833
)

custom_components/entitymap/adapters/registry.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
from homeassistant.core import HomeAssistant
88
from homeassistant.helpers import (
99
area_registry as ar,
10+
)
11+
from homeassistant.helpers import (
1012
device_registry as dr,
13+
)
14+
from homeassistant.helpers import (
1115
entity_registry as er,
1216
)
1317

@@ -63,11 +67,7 @@ async def async_populate(self, graph: DependencyGraph) -> None:
6367
GraphNode(
6468
node_id=device_node_id,
6569
node_type=NodeType.DEVICE,
66-
title=(
67-
device.name_by_user
68-
or device.name
69-
or device.id
70-
),
70+
title=(device.name_by_user or device.name or device.id),
7171
device_id=device.id,
7272
area_id=device.area_id,
7373
disabled=device.disabled_by is not None,
@@ -105,11 +105,7 @@ async def async_populate(self, graph: DependencyGraph) -> None:
105105
GraphNode(
106106
node_id=entry.entity_id,
107107
node_type=node_type,
108-
title=(
109-
entry.name
110-
or entry.original_name
111-
or entry.entity_id
112-
),
108+
title=(entry.name or entry.original_name or entry.entity_id),
113109
entity_id=entry.entity_id,
114110
device_id=entry.device_id,
115111
area_id=entry.area_id,

0 commit comments

Comments
 (0)