New apps Added
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Single source of truth for cross-entry status aggregation.
|
||||
|
||||
Both the ``maintenance_supporter/statistics`` WebSocket endpoint (which feeds
|
||||
the panel KPI chips and the Lovelace card header) and the global summary
|
||||
sensors compute their counts here, so the numbers can never diverge.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from ..const import CONF_TASKS, DOMAIN, GLOBAL_UNIQUE_ID, MaintenanceStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import MaintenanceSupporterData
|
||||
|
||||
# The status buckets we count. `ok` is included so the dashboard strategy
|
||||
# headline and a future summary sensor have a single source for it too.
|
||||
_COUNTED_STATUSES = (
|
||||
MaintenanceStatus.OVERDUE,
|
||||
MaintenanceStatus.DUE_SOON,
|
||||
MaintenanceStatus.TRIGGERED,
|
||||
MaintenanceStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
def get_object_entries(hass: HomeAssistant) -> list[ConfigEntry]:
|
||||
"""Return all non-global config entries for this domain."""
|
||||
return [entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.unique_id != GLOBAL_UNIQUE_ID]
|
||||
|
||||
|
||||
def get_runtime_data(hass: HomeAssistant, entry_id: str) -> MaintenanceSupporterData | None:
|
||||
"""Get runtime data for a config entry."""
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None:
|
||||
return None
|
||||
return getattr(entry, "runtime_data", None)
|
||||
|
||||
|
||||
def compute_status_counts(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Aggregate task status counts across every maintenance object.
|
||||
|
||||
Status counts come from the live coordinator data (``_status``), which
|
||||
already forces disabled tasks to OK. ``total_tasks`` is the configured
|
||||
task count (static), matching the historical statistics-endpoint shape.
|
||||
"""
|
||||
counts = {str(s): 0 for s in _COUNTED_STATUSES}
|
||||
total_objects = 0
|
||||
total_tasks = 0
|
||||
total_cost = 0.0
|
||||
|
||||
for entry in get_object_entries(hass):
|
||||
total_objects += 1
|
||||
# Archived tasks are inert: excluded from the task total and (via their
|
||||
# ARCHIVED _status, which isn't a counted bucket) from every status
|
||||
# count. Their cost still counts — budget is retained on archive.
|
||||
total_tasks += sum(1 for td in entry.data.get(CONF_TASKS, {}).values() if td.get("archived_at") is None)
|
||||
|
||||
rd = get_runtime_data(hass, entry.entry_id)
|
||||
coord_data = rd.coordinator.data if rd and rd.coordinator else None
|
||||
for task in (coord_data or {}).get(CONF_TASKS, {}).values():
|
||||
status = str(task.get("_status", MaintenanceStatus.OK))
|
||||
if status in counts:
|
||||
counts[status] += 1
|
||||
total_cost += task.get("_total_cost", 0.0) or 0.0
|
||||
|
||||
needs_attention = (
|
||||
counts[str(MaintenanceStatus.OVERDUE)]
|
||||
+ counts[str(MaintenanceStatus.DUE_SOON)]
|
||||
+ counts[str(MaintenanceStatus.TRIGGERED)]
|
||||
)
|
||||
|
||||
return {
|
||||
"total_objects": total_objects,
|
||||
"total_tasks": total_tasks,
|
||||
"overdue": counts[str(MaintenanceStatus.OVERDUE)],
|
||||
"due_soon": counts[str(MaintenanceStatus.DUE_SOON)],
|
||||
"triggered": counts[str(MaintenanceStatus.TRIGGERED)],
|
||||
"ok": counts[str(MaintenanceStatus.OK)],
|
||||
"needs_attention": needs_attention,
|
||||
"total_cost": round(total_cost, 2),
|
||||
}
|
||||
Reference in New Issue
Block a user