New apps Added

This commit is contained in:
2026-07-08 10:43:39 -04:00
parent 3b1f4bbd75
commit fefc2c8b5c
1114 changed files with 406637 additions and 154 deletions
@@ -0,0 +1,271 @@
"""Logbook (activity timeline) descriptions for lifecycle events.
Without this platform our bus events show up in HA's activity timeline as
raw "event maintenance_supporter_task_completed" rows. With it they read as
proper entries — *"Oil Change (Family Car) was completed — 95 €, 45 min"* —
localized like the rest of the integration, attached to the task's sensor
entity (and thereby its object's device page timeline) where possible.
"""
from __future__ import annotations
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any
from homeassistant.components.logbook.const import (
LOGBOOK_ENTRY_ENTITY_ID,
LOGBOOK_ENTRY_ICON,
LOGBOOK_ENTRY_MESSAGE,
LOGBOOK_ENTRY_NAME,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .const import (
DOMAIN,
EVENT_TASK_COMPLETED,
EVENT_TASK_RESET,
EVENT_TASK_SKIPPED,
EVENT_TRIGGER_ACTIVATED,
EVENT_TRIGGER_DEACTIVATED,
slugify_object_name,
)
from .helpers.i18n import normalize_language
if TYPE_CHECKING:
from collections.abc import Callable
# The verb phrases; everything else in a message (costs, durations, notes,
# values) is language-neutral data appended verbatim.
_STRINGS: dict[str, dict[str, str]] = {
"en": {
"completed": "was completed",
"skipped": "was skipped",
"reset": "was reset to {date}",
"trigger_on": "sensor trigger activated",
"trigger_off": "sensor trigger cleared",
},
"de": {
"completed": "wurde erledigt",
"skipped": "wurde übersprungen",
"reset": "wurde auf {date} zurückgesetzt",
"trigger_on": "Sensor-Trigger ausgelöst",
"trigger_off": "Sensor-Trigger aufgehoben",
},
"es": {
"completed": "se completó",
"skipped": "se omitió",
"reset": "se restableció a {date}",
"trigger_on": "disparador de sensor activado",
"trigger_off": "disparador de sensor desactivado",
},
"fr": {
"completed": "a été effectuée",
"skipped": "a été ignorée",
"reset": "a été réinitialisée au {date}",
"trigger_on": "déclencheur de capteur activé",
"trigger_off": "déclencheur de capteur désactivé",
},
"it": {
"completed": "è stata completata",
"skipped": "è stata saltata",
"reset": "è stata ripristinata al {date}",
"trigger_on": "trigger del sensore attivato",
"trigger_off": "trigger del sensore disattivato",
},
"nl": {
"completed": "is voltooid",
"skipped": "is overgeslagen",
"reset": "is teruggezet naar {date}",
"trigger_on": "sensortrigger geactiveerd",
"trigger_off": "sensortrigger opgeheven",
},
"pt": {
"completed": "foi concluída",
"skipped": "foi ignorada",
"reset": "foi reposta para {date}",
"trigger_on": "gatilho de sensor ativado",
"trigger_off": "gatilho de sensor desativado",
},
"ru": {
"completed": "выполнена",
"skipped": "пропущена",
"reset": "сброшена на {date}",
"trigger_on": "сенсорный триггер активирован",
"trigger_off": "сенсорный триггер деактивирован",
},
"uk": {
"completed": "виконано",
"skipped": "пропущено",
"reset": "скинуто на {date}",
"trigger_on": "сенсорний тригер активовано",
"trigger_off": "сенсорний тригер деактивовано",
},
"pl": {
"completed": "zostało ukończone",
"skipped": "zostało pominięte",
"reset": "zostało zresetowane na {date}",
"trigger_on": "wyzwalacz czujnika aktywowany",
"trigger_off": "wyzwalacz czujnika dezaktywowany",
},
"cs": {
"completed": "bylo dokončeno",
"skipped": "bylo přeskočeno",
"reset": "bylo obnoveno na {date}",
"trigger_on": "senzorový spouštěč aktivován",
"trigger_off": "senzorový spouštěč deaktivován",
},
"sv": {
"completed": "slutfördes",
"skipped": "hoppades över",
"reset": "återställdes till {date}",
"trigger_on": "sensorutlösare aktiverad",
"trigger_off": "sensorutlösare rensad",
},
"da": {
"completed": "blev fuldført",
"skipped": "blev sprunget over",
"reset": "blev nulstillet til {date}",
"trigger_on": "sensorudløser aktiveret",
"trigger_off": "sensorudløser ryddet",
},
"fi": {
"completed": "suoritettiin",
"skipped": "ohitettiin",
"reset": "palautettiin päivään {date}",
"trigger_on": "anturiliipaisin aktivoitui",
"trigger_off": "anturiliipaisin poistui",
},
"nb": {
"completed": "ble fullført",
"skipped": "ble hoppet over",
"reset": "ble tilbakestilt til {date}",
"trigger_on": "sensorutløser aktivert",
"trigger_off": "sensorutløser fjernet",
},
"ja": {
"completed": "が完了しました",
"skipped": "がスキップされました",
"reset": "{date} にリセットされました",
"trigger_on": "センサートリガーが作動しました",
"trigger_off": "センサートリガーが解除されました",
},
"hi": {
"completed": "पूरा किया गया",
"skipped": "छोड़ दिया गया",
"reset": "{date} पर रीसेट किया गया",
"trigger_on": "सेंसर ट्रिगर सक्रिय हुआ",
"trigger_off": "सेंसर ट्रिगर साफ़ हुआ",
},
"zh": {
"completed": "已完成",
"skipped": "已跳过",
"reset": "已重置为 {date}",
"trigger_on": "传感器触发器已激活",
"trigger_off": "传感器触发器已清除",
},
}
def _t(hass: HomeAssistant, key: str, **kwargs: str) -> str:
lang = normalize_language(hass)
table = _STRINGS.get(lang, _STRINGS["en"])
text = table.get(key, _STRINGS["en"][key])
return text.format(**kwargs) if kwargs else text
def _entry_name(data: Mapping[str, Any]) -> str:
task = data.get("task_name") or "?"
obj = data.get("object_name")
return f"{task} ({obj})" if obj else task
def _task_entity_id(hass: HomeAssistant, data: Mapping[str, Any]) -> str | None:
"""Resolve the task's sensor entity so the entry lands on its timeline."""
obj = data.get("object_name")
task_id = data.get("task_id")
if not obj or not task_id:
return None
unique_id = f"maintenance_supporter_{slugify_object_name(obj)}_{task_id}"
return er.async_get(hass).async_get_entity_id("sensor", DOMAIN, unique_id)
def _detail_suffix(data: Mapping[str, Any]) -> str:
"""Language-neutral completion details: cost, duration, notes."""
parts: list[str] = []
if (cost := data.get("cost")) is not None:
parts.append(str(cost))
if (duration := data.get("duration")) is not None:
parts.append(f"{duration} min")
if notes := data.get("notes"):
parts.append(str(notes))
return f"{', '.join(parts)}" if parts else ""
@callback
def async_describe_events(
hass: HomeAssistant,
async_describe_event: Callable[[str, str, Callable[[Event], dict[str, Any]]], None],
) -> None:
"""Describe the integration's bus events for the activity timeline."""
@callback
def describe_completed(event: Event) -> dict[str, Any]:
data = event.data
entry = {
LOGBOOK_ENTRY_NAME: _entry_name(data),
LOGBOOK_ENTRY_MESSAGE: _t(hass, "completed") + _detail_suffix(data),
LOGBOOK_ENTRY_ICON: "mdi:check-circle",
}
if entity_id := _task_entity_id(hass, data):
entry[LOGBOOK_ENTRY_ENTITY_ID] = entity_id
return entry
@callback
def describe_skipped(event: Event) -> dict[str, Any]:
data = event.data
message = _t(hass, "skipped")
if reason := data.get("reason"):
message += f"{reason}"
entry = {
LOGBOOK_ENTRY_NAME: _entry_name(data),
LOGBOOK_ENTRY_MESSAGE: message,
LOGBOOK_ENTRY_ICON: "mdi:skip-next-circle-outline",
}
if entity_id := _task_entity_id(hass, data):
entry[LOGBOOK_ENTRY_ENTITY_ID] = entity_id
return entry
@callback
def describe_reset(event: Event) -> dict[str, Any]:
data = event.data
entry = {
LOGBOOK_ENTRY_NAME: _entry_name(data),
LOGBOOK_ENTRY_MESSAGE: _t(hass, "reset", date=str(data.get("date") or "?")),
LOGBOOK_ENTRY_ICON: "mdi:restore",
}
if entity_id := _task_entity_id(hass, data):
entry[LOGBOOK_ENTRY_ENTITY_ID] = entity_id
return entry
@callback
def describe_trigger(event: Event) -> dict[str, Any]:
data = event.data
activated = event.event_type == EVENT_TRIGGER_ACTIVATED
message = _t(hass, "trigger_on" if activated else "trigger_off")
if activated and (value := data.get("trigger_value")) is not None:
message += f" ({data.get('trigger_entity')}: {value})"
entry = {
LOGBOOK_ENTRY_NAME: data.get("entity_id") or DOMAIN,
LOGBOOK_ENTRY_MESSAGE: message,
LOGBOOK_ENTRY_ICON: "mdi:flash" if activated else "mdi:flash-off",
}
if entity_id := data.get("entity_id"):
entry[LOGBOOK_ENTRY_ENTITY_ID] = entity_id
return entry
async_describe_event(DOMAIN, EVENT_TASK_COMPLETED, describe_completed)
async_describe_event(DOMAIN, EVENT_TASK_SKIPPED, describe_skipped)
async_describe_event(DOMAIN, EVENT_TASK_RESET, describe_reset)
async_describe_event(DOMAIN, EVENT_TRIGGER_ACTIVATED, describe_trigger)
async_describe_event(DOMAIN, EVENT_TRIGGER_DEACTIVATED, describe_trigger)