This commit is contained in:
Home Assistant Version Control
2026-07-31 16:29:09 +00:00
parent d125a48b04
commit 8c53907c88
91 changed files with 1232 additions and 387 deletions
@@ -819,6 +819,44 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
# registered in async_setup_entry. Together these two listeners keep the
# dashboard area and the HA device area in sync after the initial creation
# (DeviceInfo.suggested_area only fires once, on first device creation).
@callback
def _object_entries_for_device(
hass: HomeAssistant, device: dr.DeviceEntry
) -> list[ConfigEntry]:
"""Our object entries that live on this device.
Two ways an object reaches a device page, and they need different
lookups:
* its OWN device — our config entry is listed on it;
* a LINKED device owned by another integration — we deliberately do
NOT add our config entry there any more (HA 2026.8 forbids it), so
the only record of the link is ``obj["ha_device_id"]``. Matching on
``device.config_entries`` alone would silently stop syncing exactly
the objects the user attached by hand.
Several objects may point at the same device; all of them follow it.
"""
found: list[ConfigEntry] = []
seen: set[str] = set()
# 2026.8 gives a device exactly one config entry (`config_entry_id`);
# the plural `config_entries` is a silently-deprecated compat shim
# there — no report_usage, so the deprecation gate cannot see it — and
# the only spelling 2026.7 has. Prefer the singular where it exists.
single = getattr(device, "config_entry_id", None)
owner_ids = (single,) if single is not None else tuple(getattr(device, "config_entries", ()) or ())
for ce_id in owner_ids:
ce = hass.config_entries.async_get_entry(ce_id)
if ce is not None and ce.domain == DOMAIN and ce.unique_id != GLOBAL_UNIQUE_ID:
found.append(ce)
seen.add(ce.entry_id)
for ce in hass.config_entries.async_entries(DOMAIN):
if ce.entry_id in seen or ce.unique_id == GLOBAL_UNIQUE_ID:
continue
if (ce.data.get(CONF_OBJECT) or {}).get("ha_device_id") == device.id:
found.append(ce)
return found
@callback
def _on_device_registry_update(
event: Event[dr.EventDeviceRegistryUpdatedData],
@@ -832,17 +870,12 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
device = dr.async_get(hass).async_get(device_id)
if device is None:
return
for ce_id in device.config_entries:
ce = hass.config_entries.async_get_entry(ce_id)
if ce is None or ce.domain != DOMAIN or ce.unique_id == GLOBAL_UNIQUE_ID:
continue
for ce in _object_entries_for_device(hass, device):
obj = dict(ce.data.get(CONF_OBJECT, {}))
if obj.get("area_id") == device.area_id:
return # already in sync — break the loop with the forward listener
continue # already in sync — breaks the loop with the forward listener
obj["area_id"] = device.area_id
new_data = {**ce.data, CONF_OBJECT: obj}
hass.config_entries.async_update_entry(ce, data=new_data)
return
hass.config_entries.async_update_entry(ce, data={**ce.data, CONF_OBJECT: obj})
unsub_device = hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _on_device_registry_update)
@@ -1014,7 +1047,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
rotation tasks that were configured without an initial assignee — those
were invisible to every user filter until their first completion.
"""
if entry.version > 1 or entry.minor_version >= 4:
if entry.version > 1 or entry.minor_version >= 5:
return True
is_object_entry = entry.unique_id != GLOBAL_UNIQUE_ID
@@ -1073,6 +1106,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data[CONF_TASKS] = new_tasks
minor = 4
# 4 → 5: stop co-owning a linked device. Until now, attaching an object to
# an existing device also put OUR config entry on that device. Home
# Assistant forbids that from 2026.8 and splits such a device into one per
# config entry on upgrade — leaving a nameless extra device holding our
# entities while the appliance's page shows none. Removing the association
# here means the split never has anything to split.
if minor < 5:
if is_object_entry and (device_id := (data.get(CONF_OBJECT) or {}).get("ha_device_id")):
from .helpers.device_link import shed_owned_devices
shed_owned_devices(hass, own_entry_id=entry.entry_id, source_device_id=device_id)
minor = 5
hass.config_entries.async_update_entry(entry, data=data, minor_version=minor)
_LOGGER.info("Migrated entry %s to minor_version %s", entry.entry_id, minor)
return True
@@ -1088,6 +1134,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
await _async_setup_shared(hass)
is_global = entry.unique_id == GLOBAL_UNIQUE_ID
# True once the object's stored device link resolved to a live device this
# setup — drives the post-platform cleanup of empty leftover own devices.
linked_device_live = False
if is_global:
# Global entry: no per-object coordinator, but a summary coordinator
@@ -1189,6 +1238,81 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
)
await store.async_save()
# A device link stored before HA 2026.8 may now point at a
# pre-migration composite id, which names no registered device — HA
# refuses to attach entities to it and tells the user to file a bug
# against us. Repoint it at the surviving device and write that back,
# so the repair happens once rather than on every start.
obj_data = entry.data.get(CONF_OBJECT) or {}
if stored_device_id := obj_data.get("ha_device_id"):
from .helpers.device_link import (
has_own_devices,
resolve_linked_device_id,
shed_owned_devices,
)
resolved = resolve_linked_device_id(hass, stored_device_id, own_entry_id=entry.entry_id)
if resolved and resolved != stored_device_id:
_LOGGER.info(
"Object %s was linked to device %s, which the 2026.8 split replaced; repointed to %s",
entry.title,
stored_device_id,
resolved,
)
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_OBJECT: {**obj_data, "ha_device_id": resolved}},
)
# An id we cannot resolve is deliberately LEFT ALONE. Clearing it
# would gain nothing — a missing device already falls back to an own
# device at render time — and would destroy the user's choice for
# good. The id may well be meaningful again later: a JSON export
# carries it to a foreign instance where it dangles until the backup
# comes home, and a device can return when its integration is
# re-added or re-enabled.
#
# Falling back is quiet, though, and a link the user set on purpose
# is worth telling them about rather than letting the object drift
# onto a device of its own unannounced. The issue clears itself the
# moment the link resolves again.
from homeassistant.helpers import issue_registry as ir
issue_id = f"device_link_lost_{entry.entry_id}"
if resolved is None:
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="device_link_lost",
translation_placeholders={"object": obj_data.get("name") or entry.title},
)
else:
ir.async_delete_issue(hass, DOMAIN, issue_id)
# A linked object owns no device. If it does, this install predates
# the rework — either it co-owned the appliance's device (split on
# upgrade) or it forked one carrying the appliance's identity. Hand
# that to Home Assistant's own repair, which also relinks the
# entities, rather than deleting devices by hand.
if has_own_devices(hass, entry.entry_id):
_LOGGER.info("Shedding device(s) %s should not own after linking", entry.title)
shed_owned_devices(hass, own_entry_id=entry.entry_id, source_device_id=resolved)
linked_device_live = resolved is not None
# #115: the battery fleet's seeded name/notes/part texts are stored
# snapshots — a fleet set up before localized seeding existed (or under
# a different UI language) keeps English notes forever while every
# runtime string around them is translated. Rewrite the ones the user
# never touched into the instance's language; edited texts stay.
if obj_data.get("battery_fleet"):
from .helpers.battery_fleet_setup import retranslate_seeded_texts
from .helpers.i18n import normalize_language
if retranslate_seeded_texts(hass, entry, normalize_language(hass)):
_LOGGER.info("Retranslated the battery fleet's seeded texts for %s", entry.title)
coordinator = MaintenanceCoordinator(hass, entry, store)
entry.runtime_data = MaintenanceSupporterData(coordinator=coordinator, store=store)
await coordinator.async_config_entry_first_refresh()
@@ -1222,6 +1346,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
if linked_device_live:
# The entities of a linked object live on the appliance'''s device, so a
# device of our own with nothing on it is a leftover — of the object'''s
# earlier unlinked life, or of the 2026.8 split — and shows up as an
# empty duplicate named after the object. The targeted shed above only
# removes duplicates of the SOURCE device, so an old own device (our
# identifiers, not a fork) survives it on both HA versions. Decidable
# only now: the platform setup above has re-attached the entities.
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
for device in dr.async_entries_for_config_entry(dev_reg, entry.entry_id):
if not er.async_entries_for_device(ent_reg, device.id, include_disabled_entities=True):
_LOGGER.info("Removing empty leftover device %s of linked object %s", device.id, entry.title)
dev_reg.async_remove_device(device.id)
return True
@@ -1517,6 +1657,13 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
# as a fixable repair issue so the user can restore it (#86).
_sync_missing_global_entry_issue(hass)
return
# A "lost its device link" notice belongs to the object; deleting the object
# answers it, so it must not outlive it as an orphan in the Repairs list.
from homeassistant.helpers import issue_registry as ir
ir.async_delete_issue(hass, DOMAIN, f"device_link_lost_{entry.entry_id}")
# #111: hand any shared spare-part pool to a borrower BEFORE the Store goes
# — the stock numbers exist nowhere else. Must happen here rather than in
# the panel's delete: HA's own Configure-UI removal never reaches that path.
@@ -84,7 +84,7 @@ class MaintenanceSupporterConfigFlow(TriggerConfigMixin, ConfigFlow, domain=DOMA
"""Handle a config flow for Maintenance Supporter."""
VERSION = 1
MINOR_VERSION = 4
MINOR_VERSION = 5
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -10,6 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from ..const import CONF_OBJECT, DOMAIN, GLOBAL_UNIQUE_ID
from ..coordinator import MaintenanceCoordinator
from ..helpers import device_link
class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]):
@@ -27,16 +28,49 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]):
self._task_id = task_id
self._object_data = coordinator.entry.data.get(CONF_OBJECT, {})
# 2.19 device attachment — an object can live on an EXISTING device
# owned by another integration. Home Assistant's documented way to do
# that is to point the entity at the device and nothing else:
# https://developers.home-assistant.io/blog/2025/07/18/updated-pattern-for-helpers-linking-to-devices/
#
# We used to return the device's own identifiers from `device_info`,
# which made the registry merge our config entry onto it. HA 2026.8
# scopes identifiers PER CONFIG ENTRY, so that no longer merges — it
# creates a second, nameless device holding our entities while the
# appliance's page shows none of them. Setting `device_entry` attaches
# the entities without claiming any ownership, and works on 2026.7 too
# (`entity_platform` uses a pre-set value when `device_info` is None).
#
# Resolved ONCE here so `device_info` below and the entity's device
# cannot disagree; re-evaluated when the entry reloads, which a
# link change already triggers.
#
# Resolution goes through helpers.device_link, NOT a raw
# `async_get(stored_id)`: on HA 2026.8 `async_get` still ANSWERS for a
# pre-migration composite id by synthesising a read-only device that
# carries that dead id — attaching to it makes the entity registry
# refuse the link and log a "please create a bug report" warning. The
# resolver returns a live id or None, never the synthetic. (Called via
# the module so tests can substitute the resolver.)
self._linked_device: dr.DeviceEntry | None = None
if device_id := self._object_data.get("ha_device_id"):
resolved = device_link.resolve_linked_device_id(
coordinator.hass, device_id, own_entry_id=coordinator.entry.entry_id
)
if resolved is not None:
self._linked_device = dr.async_get(coordinator.hass).async_get(resolved)
if self._linked_device is not None:
self.device_entry = self._linked_device
@property
def device_info(self) -> DeviceInfo:
def device_info(self) -> DeviceInfo | None:
"""Return device info for this maintenance object.
Three shapes (2.19, device attachment):
* ``ha_device_id`` set → attach to that EXISTING device: return only
its identifiers/connections so the registry merges us onto it — no
name/manufacturer, which would overwrite the owning integration's
metadata (the obj→device forward-sync skips linked objects too).
* linked to an existing device → ``None``. The entity is already
attached via ``device_entry`` (see ``__init__``); describing a device
here would create one of our own instead.
* ``parent_entry_id`` set → our own device, nested under the parent
object's device via ``via_device``.
* neither → our own stand-alone device (the historical shape).
@@ -44,17 +78,13 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]):
obj = self._object_data
hass = self.coordinator.hass
if device_id := obj.get("ha_device_id"):
device = dr.async_get(hass).async_get(device_id)
if device is not None and (device.identifiers or device.connections):
info = DeviceInfo()
if device.identifiers:
info["identifiers"] = device.identifiers
if device.connections:
info["connections"] = device.connections
return info
# Linked device vanished → fall through to an own device so the
# entities never end up device-less.
if self._linked_device is not None:
return None
# A linked device that no longer exists falls through to an own device,
# so the entities are never left device-less. Core helpers drop to no
# device at all here; keeping the fallback preserves what this
# integration already did, and an object with its own device page reads
# better than one with none.
identifiers = {(DOMAIN, self.coordinator.entry.unique_id or "")}
@@ -19,6 +19,9 @@ interface BatteryRow {
days_until: number | null;
available?: boolean;
}
interface RosterRow extends BatteryRow {
status: "low" | "soon" | "ok";
}
interface Overview {
available: boolean;
configured: boolean;
@@ -26,6 +29,9 @@ interface Overview {
total: number;
low: BatteryRow[];
soon: BatteryRow[];
// Every tracked battery, whatever its state. Optional so an older backend
// simply renders no roster instead of throwing.
all?: RosterRow[];
needs_now: Record<string, number>;
needs_soon: Record<string, number>;
types: string[];
@@ -133,6 +139,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
}
}
/** The forecast as a date a person can plan with, not a day count.
* `days_until` comes from last-replaced + typical lifetime, so it is an
* estimate — the tilde in the template says so. Negative values (past the
* typical lifetime but not reported low yet) render as past dates, which
* is honest: the battery is living on borrowed time. */
private _predictedDate(daysUntil: number): string {
const when = new Date(Date.now() + daysUntil * 864e5);
return new Intl.DateTimeFormat(this._lang, { day: "numeric", month: "numeric", year: "numeric" }).format(when);
}
private _shoppingLine(needs: Record<string, number>): string {
return Object.entries(needs)
.map(([type, qty]) => `${qty}× ${type}`)
@@ -220,6 +236,41 @@ export class MaintenanceBatteryFleetSection extends LitElement {
</div>
`
: nothing}
${ov.all?.length
? html`
<details class="bf-roster">
<summary>${t("battery_fleet_all", L)} (${ov.all.length})</summary>
<div class="bf-rows">
${ov.all.map(
(b) => html`
<div class="bf-row">
<span class="bf-dev">${b.device_name}</span>
<span class="bf-status bf-${b.status}">${t("battery_fleet_status_" + b.status, L)}</span>
<span class="bf-type">${b.quantity}× ${b.battery_type}</span>
${b.level != null ? html`<span class="bf-level">${b.level}%</span>` : nothing}
${b.days_until != null
? html`<span
class="bf-predicted"
title=${t("battery_fleet_predicted_on", L).replace("{date}", this._predictedDate(b.days_until))}
>~${this._predictedDate(b.days_until)}</span
>`
: nothing}
<button
class="bf-mark bf-exclude"
title=${t("battery_fleet_exclude", L)}
.disabled=${this._marking}
@click=${() => this._setExcluded(b.entity_id, true)}
>
<ha-icon icon="mdi:eye-off-outline"></ha-icon>
</button>
</div>
`,
)}
</div>
<div class="bf-roster-hint">${t("battery_fleet_all_hint", L)}</div>
</details>
`
: nothing}
${ov.excluded?.length
? html`
<div class="bf-excluded">
@@ -363,6 +414,40 @@ export class MaintenanceBatteryFleetSection extends LitElement {
font-size: 12px;
color: var(--secondary-text-color);
}
/* The roster is a lookup list, not the headline — collapsed by default so
the section still opens on what actually needs doing. */
.bf-roster > summary {
cursor: pointer;
font-size: 13px;
color: var(--secondary-text-color);
padding: 2px 0;
}
.bf-roster-hint {
font-size: 12px;
color: var(--secondary-text-color);
padding-top: 6px;
}
.bf-status {
font-size: 11px;
padding: 1px 7px;
border-radius: 9px;
white-space: nowrap;
background: var(--secondary-background-color, rgba(127, 127, 127, 0.15));
color: var(--secondary-text-color);
}
.bf-status.bf-low {
background: var(--error-color, #f44336);
color: #fff;
}
.bf-status.bf-soon {
background: var(--warning-color, #ff9800);
color: #fff;
}
.bf-predicted {
font-size: 12px;
color: var(--secondary-text-color);
white-space: nowrap;
}
.bf-total {
font-size: 12px;
color: var(--secondary-text-color);
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Vyloučit z flotily",
"battery_fleet_excluded": "Vyloučené",
"battery_fleet_include": "Znovu sledovat",
"battery_fleet_all": "Všechny sledované baterie",
"battery_fleet_all_hint": "Vylučte zařízení už dřív, než vůbec ohlásí nízkou úroveň — vysavač, který se sám nabíjí, nebo telefon, který varuje sám.",
"battery_fleet_status_low": "Nízká",
"battery_fleet_status_soon": "Brzy",
"battery_fleet_status_ok": "V pořádku",
"battery_fleet_predicted_on": "Očekáváno kolem {date}",
"battery_fleet_total": "Sledováno baterií: {n}",
"battery_fleet_setup_button": "Flotila baterií",
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Udeluk fra flåden",
"battery_fleet_excluded": "Udelukket",
"battery_fleet_include": "Følg igen",
"battery_fleet_all": "Alle overvågede batterier",
"battery_fleet_all_hint": "Udeluk en enhed her, før den overhovedet melder lavt niveau — en støvsuger, der lader sig selv, eller en telefon, der advarer af sig selv.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
"battery_fleet_predicted_on": "Forventes omkring {date}",
"battery_fleet_total": "{n} batterier overvåges",
"battery_fleet_setup_button": "Batteriflåde",
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Aus der Flotte ausschließen",
"battery_fleet_excluded": "Ausgeschlossen",
"battery_fleet_include": "Wieder aufnehmen",
"battery_fleet_all": "Alle überwachten Batterien",
"battery_fleet_all_hint": "Ein Gerät hier ausschließen, bevor es überhaupt schwach meldet — etwa ein Sauger, der sich selbst lädt, oder ein Telefon, das von sich aus warnt.",
"battery_fleet_status_low": "Schwach",
"battery_fleet_status_soon": "Bald",
"battery_fleet_status_ok": "In Ordnung",
"battery_fleet_predicted_on": "Voraussichtlich um {date}",
"battery_fleet_total": "{n} Batterien überwacht",
"battery_fleet_setup_button": "Batterie-Flotte",
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Exclude from the fleet",
"battery_fleet_excluded": "Excluded",
"battery_fleet_include": "Track again",
"battery_fleet_all": "All tracked batteries",
"battery_fleet_all_hint": "Exclude a device here to drop it from the fleet before it ever reports low — a vacuum that recharges itself, or a phone that warns you on its own.",
"battery_fleet_status_low": "Low",
"battery_fleet_status_soon": "Soon",
"battery_fleet_status_ok": "Healthy",
"battery_fleet_predicted_on": "Expected around {date}",
"battery_fleet_total": "{n} batteries tracked",
"battery_fleet_setup_button": "Battery fleet",
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Excluir de la flota",
"battery_fleet_excluded": "Excluidas",
"battery_fleet_include": "Seguir de nuevo",
"battery_fleet_all": "Todas las pilas monitorizadas",
"battery_fleet_all_hint": "Excluye aquí un dispositivo antes de que llegue a avisar de carga baja — un aspirador que se recarga solo o un teléfono que ya avisa por su cuenta.",
"battery_fleet_status_low": "Baja",
"battery_fleet_status_soon": "Pronto",
"battery_fleet_status_ok": "Correcta",
"battery_fleet_predicted_on": "Previsto hacia {date}",
"battery_fleet_total": "{n} baterías monitorizadas",
"battery_fleet_setup_button": "Flota de baterías",
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Sulje pois kannasta",
"battery_fleet_excluded": "Pois suljetut",
"battery_fleet_include": "Seuraa uudelleen",
"battery_fleet_all": "Kaikki seuratut paristot",
"battery_fleet_all_hint": "Sulje laite pois jo ennen kuin se ilmoittaa alhaisesta varauksesta — imuri, joka lataa itsensä, tai puhelin, joka varoittaa itse.",
"battery_fleet_status_low": "Vähissä",
"battery_fleet_status_soon": "Pian",
"battery_fleet_status_ok": "Kunnossa",
"battery_fleet_predicted_on": "Odotettavissa noin {date}",
"battery_fleet_total": "{n} paristoa seurannassa",
"battery_fleet_setup_button": "Paristokanta",
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Exclure du parc",
"battery_fleet_excluded": "Exclues",
"battery_fleet_include": "Suivre à nouveau",
"battery_fleet_all": "Toutes les piles suivies",
"battery_fleet_all_hint": "Excluez un appareil ici avant même qu'il ne signale une charge faible — un aspirateur qui se recharge seul, ou un téléphone qui vous prévient lui-même.",
"battery_fleet_status_low": "Faible",
"battery_fleet_status_soon": "Bientôt",
"battery_fleet_status_ok": "Bon état",
"battery_fleet_predicted_on": "Prévu vers {date}",
"battery_fleet_total": "{n} piles suivies",
"battery_fleet_setup_button": "Parc de piles",
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "फ्लीट से बाहर करें",
"battery_fleet_excluded": "बाहर किए गए",
"battery_fleet_include": "फिर से ट्रैक करें",
"battery_fleet_all": "सभी ट्रैक की गई बैटरियाँ",
"battery_fleet_all_hint": "किसी डिवाइस को यहाँ कम बैटरी बताने से पहले ही बाहर रखें — जैसे खुद चार्ज होने वाला वैक्यूम, या खुद ही चेतावनी देने वाला फ़ोन।",
"battery_fleet_status_low": "कम",
"battery_fleet_status_soon": "जल्द",
"battery_fleet_status_ok": "ठीक",
"battery_fleet_predicted_on": "{date} के आसपास अपेक्षित",
"battery_fleet_total": "{n} बैटरियाँ ट्रैक की गईं",
"battery_fleet_setup_button": "बैटरी फ्लीट",
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Kizárás a flottából",
"battery_fleet_excluded": "Kizárva",
"battery_fleet_include": "Követés újra",
"battery_fleet_all": "Minden nyomon követett elem",
"battery_fleet_all_hint": "Zárjon ki egy eszközt már azelőtt, hogy alacsony töltöttséget jelezne — például egy magát töltő porszívót vagy egy telefont, amely magától figyelmeztet.",
"battery_fleet_status_low": "Alacsony",
"battery_fleet_status_soon": "Hamarosan",
"battery_fleet_status_ok": "Rendben",
"battery_fleet_predicted_on": "Várhatóan {date} körül",
"battery_fleet_total": "{n} elem követve",
"battery_fleet_setup_button": "Elemflotta",
"battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Escludi dal parco",
"battery_fleet_excluded": "Escluse",
"battery_fleet_include": "Traccia di nuovo",
"battery_fleet_all": "Tutte le batterie monitorate",
"battery_fleet_all_hint": "Escludi qui un dispositivo prima ancora che segnali la batteria scarica — un aspirapolvere che si ricarica da solo o un telefono che avvisa da sé.",
"battery_fleet_status_low": "Scarica",
"battery_fleet_status_soon": "A breve",
"battery_fleet_status_ok": "A posto",
"battery_fleet_predicted_on": "Previsto intorno al {date}",
"battery_fleet_total": "{n} batterie monitorate",
"battery_fleet_setup_button": "Parco batterie",
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "フリートから除外",
"battery_fleet_excluded": "除外済み",
"battery_fleet_include": "再び追跡",
"battery_fleet_all": "追跡中のすべての電池",
"battery_fleet_all_hint": "電池残量が少ないと報告される前に、ここで機器を除外できます — 自動充電するロボット掃除機や、自分で警告するスマートフォンなど。",
"battery_fleet_status_low": "残量低下",
"battery_fleet_status_soon": "まもなく",
"battery_fleet_status_ok": "良好",
"battery_fleet_predicted_on": "{date} 頃の見込み",
"battery_fleet_total": "{n} 個の電池を監視",
"battery_fleet_setup_button": "電池フリート",
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "플릿에서 제외",
"battery_fleet_excluded": "제외됨",
"battery_fleet_include": "다시 추적",
"battery_fleet_all": "추적 중인 모든 배터리",
"battery_fleet_all_hint": "배터리 부족을 알리기 전에 여기에서 기기를 제외하세요 — 스스로 충전하는 로봇청소기나 자체적으로 경고하는 휴대폰처럼.",
"battery_fleet_status_low": "부족",
"battery_fleet_status_soon": "곧",
"battery_fleet_status_ok": "정상",
"battery_fleet_predicted_on": "{date}쯤 예상",
"battery_fleet_total": "배터리 {n}개 추적 중",
"battery_fleet_setup_button": "배터리 플릿",
"battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Utelukk fra flåten",
"battery_fleet_excluded": "Utelukket",
"battery_fleet_include": "Følg igjen",
"battery_fleet_all": "Alle overvåkede batterier",
"battery_fleet_all_hint": "Utelukk en enhet her før den i det hele tatt melder lavt nivå — en støvsuger som lader seg selv, eller en telefon som varsler av seg selv.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
"battery_fleet_predicted_on": "Forventes rundt {date}",
"battery_fleet_total": "{n} batterier spores",
"battery_fleet_setup_button": "Batteriflåte",
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Uitsluiten van de vloot",
"battery_fleet_excluded": "Uitgesloten",
"battery_fleet_include": "Opnieuw volgen",
"battery_fleet_all": "Alle gevolgde batterijen",
"battery_fleet_all_hint": "Sluit een apparaat hier uit voordat het ooit laag meldt — een stofzuiger die zichzelf oplaadt, of een telefoon die zelf waarschuwt.",
"battery_fleet_status_low": "Laag",
"battery_fleet_status_soon": "Binnenkort",
"battery_fleet_status_ok": "In orde",
"battery_fleet_predicted_on": "Verwacht rond {date}",
"battery_fleet_total": "{n} batterijen gevolgd",
"battery_fleet_setup_button": "Batterijvloot",
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Wyklucz z floty",
"battery_fleet_excluded": "Wykluczone",
"battery_fleet_include": "Śledź ponownie",
"battery_fleet_all": "Wszystkie śledzone baterie",
"battery_fleet_all_hint": "Wyklucz tutaj urządzenie, zanim w ogóle zgłosi niski poziom — odkurzacz, który sam się ładuje, albo telefon, który ostrzega samodzielnie.",
"battery_fleet_status_low": "Niski",
"battery_fleet_status_soon": "Wkrótce",
"battery_fleet_status_ok": "W porządku",
"battery_fleet_predicted_on": "Przewidywane około {date}",
"battery_fleet_total": "Śledzone baterie: {n}",
"battery_fleet_setup_button": "Flota baterii",
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Remover da frota",
"battery_fleet_excluded": "Removida",
"battery_fleet_include": "Acompanhar novamente",
"battery_fleet_all": "Todas as baterias monitoradas",
"battery_fleet_all_hint": "Exclua aqui um dispositivo antes que ele sequer indique carga baixa — um aspirador que se recarrega sozinho ou um celular que já avisa sozinho.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
"battery_fleet_predicted_on": "Previsto por volta de {date}",
"battery_fleet_total": "{n} baterias acompanhadas",
"battery_fleet_setup_button": "Frota de baterias",
"battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Excluir da frota",
"battery_fleet_excluded": "Excluídas",
"battery_fleet_include": "Acompanhar novamente",
"battery_fleet_all": "Todas as pilhas monitorizadas",
"battery_fleet_all_hint": "Exclua aqui um dispositivo antes de ele sequer indicar carga fraca — um aspirador que se recarrega sozinho ou um telemóvel que já avisa por si.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
"battery_fleet_predicted_on": "Previsto por volta de {date}",
"battery_fleet_total": "{n} baterias monitorizadas",
"battery_fleet_setup_button": "Frota de baterias",
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Исключить из парка",
"battery_fleet_excluded": "Исключены",
"battery_fleet_include": "Снова отслеживать",
"battery_fleet_all": "Все отслеживаемые батареи",
"battery_fleet_all_hint": "Исключите устройство здесь ещё до того, как оно сообщит о низком заряде — робот-пылесос, который заряжается сам, или телефон, который предупреждает вас сам.",
"battery_fleet_status_low": "Низкий",
"battery_fleet_status_soon": "Скоро",
"battery_fleet_status_ok": "В норме",
"battery_fleet_predicted_on": "Ожидается примерно {date}",
"battery_fleet_total": "Отслеживается батарей: {n}",
"battery_fleet_setup_button": "Парк батарей",
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Uteslut från flottan",
"battery_fleet_excluded": "Uteslutna",
"battery_fleet_include": "Spåra igen",
"battery_fleet_all": "Alla bevakade batterier",
"battery_fleet_all_hint": "Uteslut en enhet här redan innan den rapporterar låg nivå — en dammsugare som laddar sig själv, eller en telefon som varnar på egen hand.",
"battery_fleet_status_low": "Låg",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I ordning",
"battery_fleet_predicted_on": "Väntas omkring {date}",
"battery_fleet_total": "{n} batterier spåras",
"battery_fleet_setup_button": "Batteriflotta",
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Filodan hariç tut",
"battery_fleet_excluded": "Hariç tutuldu",
"battery_fleet_include": "Yeniden takip et",
"battery_fleet_all": "İzlenen tüm piller",
"battery_fleet_all_hint": "Bir cihazı, daha düşük şarj bildirmeden burada hariç tutun — kendi kendine şarj olan bir süpürge ya da zaten kendisi uyaran bir telefon gibi.",
"battery_fleet_status_low": "Düşük",
"battery_fleet_status_soon": "Yakında",
"battery_fleet_status_ok": "İyi durumda",
"battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor",
"battery_fleet_total": "{n} pil takip ediliyor",
"battery_fleet_setup_button": "Pil filosu",
"battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Виключити з парку",
"battery_fleet_excluded": "Виключені",
"battery_fleet_include": "Знову відстежувати",
"battery_fleet_all": "Усі відстежувані батареї",
"battery_fleet_all_hint": "Вилучіть пристрій тут ще до того, як він повідомить про низький заряд — робот-пилосос, що заряджається сам, або телефон, який попереджає сам.",
"battery_fleet_status_low": "Низький",
"battery_fleet_status_soon": "Незабаром",
"battery_fleet_status_ok": "У нормі",
"battery_fleet_predicted_on": "Очікується приблизно {date}",
"battery_fleet_total": "Відстежується батарей: {n}",
"battery_fleet_setup_button": "Парк батарей",
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "从电池群中排除",
"battery_fleet_excluded": "已排除",
"battery_fleet_include": "重新跟踪",
"battery_fleet_all": "所有跟踪的电池",
"battery_fleet_all_hint": "在设备报告电量不足之前就可以在这里将其排除——例如会自行充电的扫地机器人,或本身就会提醒你的手机。",
"battery_fleet_status_low": "电量低",
"battery_fleet_status_soon": "即将",
"battery_fleet_status_ok": "正常",
"battery_fleet_predicted_on": "预计在 {date} 前后",
"battery_fleet_total": "已跟踪 {n} 个电池",
"battery_fleet_setup_button": "电池群",
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。",
@@ -20,7 +20,9 @@
* show_user_filter: true # default true
* user_filter: "" # "" | "current_user" | "<uuid>"
* show_object_filter: true # default true; dropdown appears with 2+ objects
* object_filter: "" # "" | "<entry_id>" | "<object name>" (Discussion #83)
* object_filter: "" # "" | "<entry_id>" | "<object name>" — or a LIST of
* # them to restrict the card to several objects; the
* # dropdown then narrows within that set (Discussion #83)
*/
import { LitElement, html, css, nothing } from "lit";
@@ -51,7 +53,7 @@ interface CalendarCardConfig {
user_filter?: string;
/** Discussion #83 — in-card object dropdown + optional preselect. */
show_object_filter?: boolean;
object_filter?: string;
object_filter?: string | string[];
/** v2.2.0 — when set, the card renders past N days from history instead
* of forward N days from next_due. Mutually exclusive with window_days. */
past_days?: PastDays;
@@ -68,6 +70,8 @@ export class MaintenanceCalendarCard extends LitElement {
@state() private _pastDays: PastDays | 0 = 0; // 0 = forward mode
@state() private _userFilter = "";
@state() private _objectFilter = "";
// 2+ configured object_filter values — restricts the card to this set.
private _configuredObjects: string[] = [];
@state() private _unsub: (() => void) | null = null;
private _dataLoaded = false;
@@ -100,6 +104,15 @@ export class MaintenanceCalendarCard extends LitElement {
}
if (typeof config.object_filter === "string") {
this._objectFilter = config.object_filter;
this._configuredObjects = [];
} else if (Array.isArray(config.object_filter)) {
const values = config.object_filter.filter((v): v is string => typeof v === "string" && v !== "");
// One entry behaves exactly like the string form (pre-select, dropdown
// over all objects); two or more RESTRICT the card to that set and the
// dropdown narrows within it — the multi-object parity with the task
// card's filter_objects that #83 asked for.
this._objectFilter = values.length === 1 ? values[0] : "";
this._configuredObjects = values.length > 1 ? values : [];
}
}
@@ -236,16 +249,28 @@ export class MaintenanceCalendarCard extends LitElement {
// Object filter (#83): the config value may be an entry_id or an object
// NAME (friendlier in hand-written YAML) — resolve against the loaded
// objects; an unknown value falls back to "all" instead of a blank card.
const showObjectFilter = this._config.show_object_filter !== false && this._objects.length > 1;
let filterEntryId: string | null = null;
if (this._objectFilter) {
const needle = this._objectFilter.toLowerCase();
const resolve = (value: string): string | null => {
const needle = value.toLowerCase();
const match = this._objects.find(
(o) => o.entry_id === this._objectFilter || o.object.name.toLowerCase() === needle,
(o) => o.entry_id === value || o.object.name.toLowerCase() === needle,
);
filterEntryId = match?.entry_id ?? null;
}
const objects = filterEntryId ? this._objects.filter((o) => o.entry_id === filterEntryId) : this._objects;
return match?.entry_id ?? null;
};
// A configured LIST restricts the card to that set; unknown names resolve
// to nothing and an entirely-unresolved list falls back to all objects
// rather than a blank card (same rule the single value always had).
const configuredIds = new Set(
this._configuredObjects.map(resolve).filter((id): id is string => id !== null),
);
const base = configuredIds.size
? this._objects.filter((o) => configuredIds.has(o.entry_id))
: this._objects;
const showObjectFilter = this._config.show_object_filter !== false && base.length > 1;
const filterEntryId = this._objectFilter ? resolve(this._objectFilter) : null;
const objects =
filterEntryId && base.some((o) => o.entry_id === filterEntryId)
? base.filter((o) => o.entry_id === filterEntryId)
: base;
const today = new Date();
today.setHours(0, 0, 0, 0);
@@ -394,7 +419,7 @@ export class MaintenanceCalendarCard extends LitElement {
this._objectFilter = (e.target as HTMLSelectElement).value;
}}>
<option value="">${t("all_objects", L)}</option>
${[...this._objects]
${[...base]
.sort((a, b) => a.object.name.localeCompare(b.object.name))
.map(
(o) => html`<option value=${o.entry_id} ?selected=${o.entry_id === filterEntryId}>${o.object.name}</option>`,
@@ -569,7 +594,8 @@ class MaintenanceCalendarCardEditor extends LitElement {
/>
</div>
<div class="hint">
Pre-select one object via YAML: object_filter: "&lt;object name&gt;".
Pre-select one object via YAML: object_filter: "&lt;object name&gt;" — or a
list of names to restrict the card to several objects.
</div>
</div>
`;
@@ -5,7 +5,9 @@ from __future__ import annotations
import logging
from pathlib import Path
from homeassistant.components.http import StaticPathConfig
# Valid at runtime on HA 2026.7 and 2026.8; see the note in ../panel.py for why
# the re-export is silenced instead of importing from a different path.
from homeassistant.components.http import StaticPathConfig # type: ignore[attr-defined]
from homeassistant.core import HomeAssistant
from ..const import (
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Vyloučit z flotily",
"battery_fleet_excluded": "Vyloučené",
"battery_fleet_include": "Znovu sledovat",
"battery_fleet_all": "Všechny sledované baterie",
"battery_fleet_all_hint": "Vylučte zařízení už dřív, než vůbec ohlásí nízkou úroveň — vysavač, který se sám nabíjí, nebo telefon, který varuje sám.",
"battery_fleet_status_low": "Nízká",
"battery_fleet_status_soon": "Brzy",
"battery_fleet_status_ok": "V pořádku",
"battery_fleet_predicted_on": "Očekáváno kolem {date}",
"battery_fleet_total": "Sledováno baterií: {n}",
"battery_fleet_setup_button": "Flotila baterií",
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Udeluk fra flåden",
"battery_fleet_excluded": "Udelukket",
"battery_fleet_include": "Følg igen",
"battery_fleet_all": "Alle overvågede batterier",
"battery_fleet_all_hint": "Udeluk en enhed her, før den overhovedet melder lavt niveau — en støvsuger, der lader sig selv, eller en telefon, der advarer af sig selv.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
"battery_fleet_predicted_on": "Forventes omkring {date}",
"battery_fleet_total": "{n} batterier overvåges",
"battery_fleet_setup_button": "Batteriflåde",
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Aus der Flotte ausschließen",
"battery_fleet_excluded": "Ausgeschlossen",
"battery_fleet_include": "Wieder aufnehmen",
"battery_fleet_all": "Alle überwachten Batterien",
"battery_fleet_all_hint": "Ein Gerät hier ausschließen, bevor es überhaupt schwach meldet — etwa ein Sauger, der sich selbst lädt, oder ein Telefon, das von sich aus warnt.",
"battery_fleet_status_low": "Schwach",
"battery_fleet_status_soon": "Bald",
"battery_fleet_status_ok": "In Ordnung",
"battery_fleet_predicted_on": "Voraussichtlich um {date}",
"battery_fleet_total": "{n} Batterien überwacht",
"battery_fleet_setup_button": "Batterie-Flotte",
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Exclude from the fleet",
"battery_fleet_excluded": "Excluded",
"battery_fleet_include": "Track again",
"battery_fleet_all": "All tracked batteries",
"battery_fleet_all_hint": "Exclude a device here to drop it from the fleet before it ever reports low — a vacuum that recharges itself, or a phone that warns you on its own.",
"battery_fleet_status_low": "Low",
"battery_fleet_status_soon": "Soon",
"battery_fleet_status_ok": "Healthy",
"battery_fleet_predicted_on": "Expected around {date}",
"battery_fleet_total": "{n} batteries tracked",
"battery_fleet_setup_button": "Battery fleet",
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Excluir de la flota",
"battery_fleet_excluded": "Excluidas",
"battery_fleet_include": "Seguir de nuevo",
"battery_fleet_all": "Todas las pilas monitorizadas",
"battery_fleet_all_hint": "Excluye aquí un dispositivo antes de que llegue a avisar de carga baja — un aspirador que se recarga solo o un teléfono que ya avisa por su cuenta.",
"battery_fleet_status_low": "Baja",
"battery_fleet_status_soon": "Pronto",
"battery_fleet_status_ok": "Correcta",
"battery_fleet_predicted_on": "Previsto hacia {date}",
"battery_fleet_total": "{n} baterías monitorizadas",
"battery_fleet_setup_button": "Flota de baterías",
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Sulje pois kannasta",
"battery_fleet_excluded": "Pois suljetut",
"battery_fleet_include": "Seuraa uudelleen",
"battery_fleet_all": "Kaikki seuratut paristot",
"battery_fleet_all_hint": "Sulje laite pois jo ennen kuin se ilmoittaa alhaisesta varauksesta — imuri, joka lataa itsensä, tai puhelin, joka varoittaa itse.",
"battery_fleet_status_low": "Vähissä",
"battery_fleet_status_soon": "Pian",
"battery_fleet_status_ok": "Kunnossa",
"battery_fleet_predicted_on": "Odotettavissa noin {date}",
"battery_fleet_total": "{n} paristoa seurannassa",
"battery_fleet_setup_button": "Paristokanta",
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Exclure du parc",
"battery_fleet_excluded": "Exclues",
"battery_fleet_include": "Suivre à nouveau",
"battery_fleet_all": "Toutes les piles suivies",
"battery_fleet_all_hint": "Excluez un appareil ici avant même qu'il ne signale une charge faible — un aspirateur qui se recharge seul, ou un téléphone qui vous prévient lui-même.",
"battery_fleet_status_low": "Faible",
"battery_fleet_status_soon": "Bientôt",
"battery_fleet_status_ok": "Bon état",
"battery_fleet_predicted_on": "Prévu vers {date}",
"battery_fleet_total": "{n} piles suivies",
"battery_fleet_setup_button": "Parc de piles",
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "फ्लीट से बाहर करें",
"battery_fleet_excluded": "बाहर किए गए",
"battery_fleet_include": "फिर से ट्रैक करें",
"battery_fleet_all": "सभी ट्रैक की गई बैटरियाँ",
"battery_fleet_all_hint": "किसी डिवाइस को यहाँ कम बैटरी बताने से पहले ही बाहर रखें — जैसे खुद चार्ज होने वाला वैक्यूम, या खुद ही चेतावनी देने वाला फ़ोन।",
"battery_fleet_status_low": "कम",
"battery_fleet_status_soon": "जल्द",
"battery_fleet_status_ok": "ठीक",
"battery_fleet_predicted_on": "{date} के आसपास अपेक्षित",
"battery_fleet_total": "{n} बैटरियाँ ट्रैक की गईं",
"battery_fleet_setup_button": "बैटरी फ्लीट",
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Kizárás a flottából",
"battery_fleet_excluded": "Kizárva",
"battery_fleet_include": "Követés újra",
"battery_fleet_all": "Minden nyomon követett elem",
"battery_fleet_all_hint": "Zárjon ki egy eszközt már azelőtt, hogy alacsony töltöttséget jelezne — például egy magát töltő porszívót vagy egy telefont, amely magától figyelmeztet.",
"battery_fleet_status_low": "Alacsony",
"battery_fleet_status_soon": "Hamarosan",
"battery_fleet_status_ok": "Rendben",
"battery_fleet_predicted_on": "Várhatóan {date} körül",
"battery_fleet_total": "{n} elem követve",
"battery_fleet_setup_button": "Elemflotta",
"battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Escludi dal parco",
"battery_fleet_excluded": "Escluse",
"battery_fleet_include": "Traccia di nuovo",
"battery_fleet_all": "Tutte le batterie monitorate",
"battery_fleet_all_hint": "Escludi qui un dispositivo prima ancora che segnali la batteria scarica — un aspirapolvere che si ricarica da solo o un telefono che avvisa da sé.",
"battery_fleet_status_low": "Scarica",
"battery_fleet_status_soon": "A breve",
"battery_fleet_status_ok": "A posto",
"battery_fleet_predicted_on": "Previsto intorno al {date}",
"battery_fleet_total": "{n} batterie monitorate",
"battery_fleet_setup_button": "Parco batterie",
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "フリートから除外",
"battery_fleet_excluded": "除外済み",
"battery_fleet_include": "再び追跡",
"battery_fleet_all": "追跡中のすべての電池",
"battery_fleet_all_hint": "電池残量が少ないと報告される前に、ここで機器を除外できます — 自動充電するロボット掃除機や、自分で警告するスマートフォンなど。",
"battery_fleet_status_low": "残量低下",
"battery_fleet_status_soon": "まもなく",
"battery_fleet_status_ok": "良好",
"battery_fleet_predicted_on": "{date} 頃の見込み",
"battery_fleet_total": "{n} 個の電池を監視",
"battery_fleet_setup_button": "電池フリート",
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "플릿에서 제외",
"battery_fleet_excluded": "제외됨",
"battery_fleet_include": "다시 추적",
"battery_fleet_all": "추적 중인 모든 배터리",
"battery_fleet_all_hint": "배터리 부족을 알리기 전에 여기에서 기기를 제외하세요 — 스스로 충전하는 로봇청소기나 자체적으로 경고하는 휴대폰처럼.",
"battery_fleet_status_low": "부족",
"battery_fleet_status_soon": "곧",
"battery_fleet_status_ok": "정상",
"battery_fleet_predicted_on": "{date}쯤 예상",
"battery_fleet_total": "배터리 {n}개 추적 중",
"battery_fleet_setup_button": "배터리 플릿",
"battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Utelukk fra flåten",
"battery_fleet_excluded": "Utelukket",
"battery_fleet_include": "Følg igjen",
"battery_fleet_all": "Alle overvåkede batterier",
"battery_fleet_all_hint": "Utelukk en enhet her før den i det hele tatt melder lavt nivå — en støvsuger som lader seg selv, eller en telefon som varsler av seg selv.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
"battery_fleet_predicted_on": "Forventes rundt {date}",
"battery_fleet_total": "{n} batterier spores",
"battery_fleet_setup_button": "Batteriflåte",
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Uitsluiten van de vloot",
"battery_fleet_excluded": "Uitgesloten",
"battery_fleet_include": "Opnieuw volgen",
"battery_fleet_all": "Alle gevolgde batterijen",
"battery_fleet_all_hint": "Sluit een apparaat hier uit voordat het ooit laag meldt — een stofzuiger die zichzelf oplaadt, of een telefoon die zelf waarschuwt.",
"battery_fleet_status_low": "Laag",
"battery_fleet_status_soon": "Binnenkort",
"battery_fleet_status_ok": "In orde",
"battery_fleet_predicted_on": "Verwacht rond {date}",
"battery_fleet_total": "{n} batterijen gevolgd",
"battery_fleet_setup_button": "Batterijvloot",
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Wyklucz z floty",
"battery_fleet_excluded": "Wykluczone",
"battery_fleet_include": "Śledź ponownie",
"battery_fleet_all": "Wszystkie śledzone baterie",
"battery_fleet_all_hint": "Wyklucz tutaj urządzenie, zanim w ogóle zgłosi niski poziom — odkurzacz, który sam się ładuje, albo telefon, który ostrzega samodzielnie.",
"battery_fleet_status_low": "Niski",
"battery_fleet_status_soon": "Wkrótce",
"battery_fleet_status_ok": "W porządku",
"battery_fleet_predicted_on": "Przewidywane około {date}",
"battery_fleet_total": "Śledzone baterie: {n}",
"battery_fleet_setup_button": "Flota baterii",
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Remover da frota",
"battery_fleet_excluded": "Removida",
"battery_fleet_include": "Acompanhar novamente",
"battery_fleet_all": "Todas as baterias monitoradas",
"battery_fleet_all_hint": "Exclua aqui um dispositivo antes que ele sequer indique carga baixa — um aspirador que se recarrega sozinho ou um celular que já avisa sozinho.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
"battery_fleet_predicted_on": "Previsto por volta de {date}",
"battery_fleet_total": "{n} baterias acompanhadas",
"battery_fleet_setup_button": "Frota de baterias",
"battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Excluir da frota",
"battery_fleet_excluded": "Excluídas",
"battery_fleet_include": "Acompanhar novamente",
"battery_fleet_all": "Todas as pilhas monitorizadas",
"battery_fleet_all_hint": "Exclua aqui um dispositivo antes de ele sequer indicar carga fraca — um aspirador que se recarrega sozinho ou um telemóvel que já avisa por si.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
"battery_fleet_predicted_on": "Previsto por volta de {date}",
"battery_fleet_total": "{n} baterias monitorizadas",
"battery_fleet_setup_button": "Frota de baterias",
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Исключить из парка",
"battery_fleet_excluded": "Исключены",
"battery_fleet_include": "Снова отслеживать",
"battery_fleet_all": "Все отслеживаемые батареи",
"battery_fleet_all_hint": "Исключите устройство здесь ещё до того, как оно сообщит о низком заряде — робот-пылесос, который заряжается сам, или телефон, который предупреждает вас сам.",
"battery_fleet_status_low": "Низкий",
"battery_fleet_status_soon": "Скоро",
"battery_fleet_status_ok": "В норме",
"battery_fleet_predicted_on": "Ожидается примерно {date}",
"battery_fleet_total": "Отслеживается батарей: {n}",
"battery_fleet_setup_button": "Парк батарей",
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Uteslut från flottan",
"battery_fleet_excluded": "Uteslutna",
"battery_fleet_include": "Spåra igen",
"battery_fleet_all": "Alla bevakade batterier",
"battery_fleet_all_hint": "Uteslut en enhet här redan innan den rapporterar låg nivå — en dammsugare som laddar sig själv, eller en telefon som varnar på egen hand.",
"battery_fleet_status_low": "Låg",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I ordning",
"battery_fleet_predicted_on": "Väntas omkring {date}",
"battery_fleet_total": "{n} batterier spåras",
"battery_fleet_setup_button": "Batteriflotta",
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Filodan hariç tut",
"battery_fleet_excluded": "Hariç tutuldu",
"battery_fleet_include": "Yeniden takip et",
"battery_fleet_all": "İzlenen tüm piller",
"battery_fleet_all_hint": "Bir cihazı, daha düşük şarj bildirmeden burada hariç tutun — kendi kendine şarj olan bir süpürge ya da zaten kendisi uyaran bir telefon gibi.",
"battery_fleet_status_low": "Düşük",
"battery_fleet_status_soon": "Yakında",
"battery_fleet_status_ok": "İyi durumda",
"battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor",
"battery_fleet_total": "{n} pil takip ediliyor",
"battery_fleet_setup_button": "Pil filosu",
"battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "Виключити з парку",
"battery_fleet_excluded": "Виключені",
"battery_fleet_include": "Знову відстежувати",
"battery_fleet_all": "Усі відстежувані батареї",
"battery_fleet_all_hint": "Вилучіть пристрій тут ще до того, як він повідомить про низький заряд — робот-пилосос, що заряджається сам, або телефон, який попереджає сам.",
"battery_fleet_status_low": "Низький",
"battery_fleet_status_soon": "Незабаром",
"battery_fleet_status_ok": "У нормі",
"battery_fleet_predicted_on": "Очікується приблизно {date}",
"battery_fleet_total": "Відстежується батарей: {n}",
"battery_fleet_setup_button": "Парк батарей",
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.",
@@ -814,6 +814,12 @@
"battery_fleet_exclude": "从电池群中排除",
"battery_fleet_excluded": "已排除",
"battery_fleet_include": "重新跟踪",
"battery_fleet_all": "所有跟踪的电池",
"battery_fleet_all_hint": "在设备报告电量不足之前就可以在这里将其排除——例如会自行充电的扫地机器人,或本身就会提醒你的手机。",
"battery_fleet_status_low": "电量低",
"battery_fleet_status_soon": "即将",
"battery_fleet_status_ok": "正常",
"battery_fleet_predicted_on": "预计在 {date} 前后",
"battery_fleet_total": "已跟踪 {n} 个电池",
"battery_fleet_setup_button": "电池群",
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.44.1 */
/*! maintenance_supporter frontend 2.46.0 */
var d="maintenance-supporter",C=`ll-strategy-dashboard-${d}`,L="hui-maintenance-supporter-strategy-editor",D="/maintenance_supporter_strategy/maintenance-dashboard-strategy.js",m=null;function k(){return m||(m=import(D)),m}async function N(){let r=await k();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await k(),document.createElement(L)}static async generate(c,h){return(await N()).generate(c,h)}};function M(){try{customElements.define(C,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===d&&r.strategyType==="dashboard")||w.customStrategies.push({type:d,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,h=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${d}`;function E(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let l of Array.from(i))t.push(l)}return null}function S(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function R(){try{let t=E("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=S(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function _(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let l=e.pop();if(i++,!l)continue;let u=l;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&h.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let b=l.children;if(b)for(let s of Array.from(b))e.push(s)}return n?!0:o?!1:a&&t<3&&R()}let A="/maintenance_supporter_strategy_shim.js",y=0,v=0;function T(){let a=Date.now();a-v<5e3||y>=3||(v=a,y+=1,import(`${A}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function f(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}_()?T():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",f):f(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||f()})}catch{}})();
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.44.1 */
import{a as v}from"./chunk-TF44IC56.js";import{a as u,b as o,d as h,e as b,f as y,g as l,h as g,i as r,k as m,t as p}from"./chunk-RLJYCMAK.js";import{a as n}from"./chunk-KRBXISES.js";var x=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=parseFloat(this._localMonthly),a=parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(a)&&a>=0&&(i.budget_yearly=a),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,a=this._status;if(!a)return o`<ha-card><div class="loading">${r("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=a.currency_symbol||g,_=a.alert_threshold_pct??x,f=[{label:r("budget_monthly",t)||"Monthly",spent:a.monthly_spent||0,budget:a.monthly_budget||0},{label:r("budget_yearly",t)||"Yearly",spent:a.yearly_spent||0,budget:a.yearly_budget||0}];return o`
/*! maintenance_supporter frontend 2.46.0 */
import{a as v}from"./chunk-V27MBWM3.js";import{a as u,b as o,d as h,e as b,f as y,g as l,h as g,i as r,k as m,t as p}from"./chunk-DM35MPY4.js";import{a as n}from"./chunk-JSL2OXKU.js";var x=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=parseFloat(this._localMonthly),a=parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(a)&&a>=0&&(i.budget_yearly=a),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,a=this._status;if(!a)return o`<ha-card><div class="loading">${r("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=a.currency_symbol||g,_=a.alert_threshold_pct??x,f=[{label:r("budget_monthly",t)||"Monthly",spent:a.monthly_spent||0,budget:a.monthly_budget||0},{label:r("budget_yearly",t)||"Yearly",spent:a.yearly_spent||0,budget:a.yearly_budget||0}];return o`
<ha-card>
<div class="card-content">
<div class="header">
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.44.1 */
/*! maintenance_supporter frontend 2.46.0 */
var s=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var t=(a,r,c,o)=>{for(var e=o>1?void 0:o?l(r,c):r,i=a.length-1,d;i>=0;i--)(d=a[i])&&(e=(o?d(r,c,e):d(e))||e);return o&&e&&s(r,c,e),e};var m={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},v={ok:"mdi:check-circle",due_soon:"mdi:alert-circle",overdue:"mdi:alert-octagon",triggered:"mdi:bell-alert",archived:"mdi:archive-outline",paused:"mdi:pause-circle-outline",completed:"mdi:check-circle",skipped:"mdi:skip-next",missed:"mdi:calendar-remove",reset:"mdi:refresh"};export{t as a,m as b,v as c};
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.44.1 */
import{a as r}from"./chunk-RLJYCMAK.js";var a=r`
/*! maintenance_supporter frontend 2.46.0 */
import{a as r}from"./chunk-DM35MPY4.js";var a=r`
ha-card { overflow: hidden; }
.card-content {
padding: 16px;
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.44.1 */
import{a as m}from"./chunk-TF44IC56.js";import{a as u,b as s,d as l,e as h,f as g,g as o,i,k as _,t as p}from"./chunk-RLJYCMAK.js";import{a}from"./chunk-KRBXISES.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),_(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
/*! maintenance_supporter frontend 2.46.0 */
import{a as m}from"./chunk-V27MBWM3.js";import{a as u,b as s,d as l,e as h,f as g,g as o,i,k as _,t as p}from"./chunk-DM35MPY4.js";import{a}from"./chunk-JSL2OXKU.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),_(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
<ha-card>
<div class="card-content">
<div class="header">
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.44.1 */
import{a as m}from"./chunk-TF44IC56.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,k as f,t as l}from"./chunk-RLJYCMAK.js";import{a as i}from"./chunk-KRBXISES.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),f(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,b=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",g=p?"active":d?"scheduled":"inactive";return n`
/*! maintenance_supporter frontend 2.46.0 */
import{a as m}from"./chunk-V27MBWM3.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,k as f,t as l}from"./chunk-DM35MPY4.js";import{a as i}from"./chunk-JSL2OXKU.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),f(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,b=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",g=p?"active":d?"scheduled":"inactive";return n`
<ha-card>
<div class="card-content">
<div class="header">
File diff suppressed because one or more lines are too long
@@ -131,6 +131,14 @@ class BatteryOverview:
total: int = 0
low: list[dict[str, Any]] = field(default_factory=list)
soon: list[dict[str, Any]] = field(default_factory=list)
# EVERY tracked battery, each tagged low / soon / ok, device-name sorted.
#
# low and soon answer "what needs doing"; this answers "what is the fleet
# watching". Without it a device that is perfectly healthy appears nowhere
# in the payload, so it could only be excluded once it had already gone low
# — which is exactly when it is being noisy, and after the fleet task may
# have auto-completed and dropped it from the list again (discussion #113).
all: list[dict[str, Any]] = field(default_factory=list)
# Grouped quantities by canonical type.
needs_now: OrderedDict[str, int] = field(default_factory=OrderedDict)
needs_soon: OrderedDict[str, int] = field(default_factory=OrderedDict)
@@ -167,6 +175,8 @@ def build_overview(
A battery already low is never double-counted into soon.
* ``needs_now`` / ``needs_soon`` = summed quantities per type the shopping
grouping ("2× AA, 4× AAA").
* ``all`` = every battery with its status, so a healthy device can be
excluded BEFORE it ever becomes noisy.
"""
ov = BatteryOverview(total=len(batteries))
types_seen: OrderedDict[str, None] = OrderedDict()
@@ -174,16 +184,20 @@ def build_overview(
for bat in sorted(batteries, key=lambda b: b.device_name.lower()):
t = _norm_type(bat.battery_type)
types_seen[t] = None
pred = _predicted_date(bat)
days = (pred - today).days if pred is not None else None
if bat.low:
ov.low.append(_row(bat, t, None))
ov.needs_now[t] = ov.needs_now.get(t, 0) + bat.quantity
# A battery reported low has no meaningful forecast left to show.
ov.all.append({**_row(bat, t, None), "status": "low"})
continue
pred = _predicted_date(bat)
if pred is not None:
days = (pred - today).days
if days <= horizon_days:
ov.soon.append(_row(bat, t, days))
ov.needs_soon[t] = ov.needs_soon.get(t, 0) + bat.quantity
if days is not None and days <= horizon_days:
ov.soon.append(_row(bat, t, days))
ov.needs_soon[t] = ov.needs_soon.get(t, 0) + bat.quantity
ov.all.append({**_row(bat, t, days), "status": "soon"})
continue
ov.all.append({**_row(bat, t, days), "status": "ok"})
ov.soon.sort(key=lambda r: r["days_until"] if r["days_until"] is not None else 1 << 30)
ov.types = sorted(types_seen)
@@ -170,6 +170,113 @@ def _type_part(btype: str, total_qty: int, lang: str) -> dict[str, Any]:
}
# ── retranslating the seeded texts (issue #115) ────────────────────────────
#
# Setup writes the object/task/part names and notes ONCE, in the language that
# happened to be current — and nothing ever touched them again, so a fleet set
# up before localized seeding existed (v2.38) keeps English notes forever while
# every runtime string around them is translated. The panel's runtime strings
# follow the UI language; these stored ones can only follow it if something
# rewrites them.
#
# The rule that makes rewriting safe: a stored text is only replaced when it
# EXACTLY matches one of the known template variants (any language), i.e. the
# user never edited it. Anything the user typed stays.
def _template_variants(text_en: str) -> set[str]:
"""The English template plus every translation of it."""
from ..templates_i18n import _T
return {text_en} | set(_T.get(text_en, {}).values())
def _extract_placeholder(stored: str, text_en: str, placeholder: str) -> str | None:
"""The placeholder's value, if ``stored`` matches any variant of the template.
``"AA-batteri"`` against ``"{type} battery"`` finds the Danish variant
``"{type}-batteri"``, matches prefix/suffix and returns ``"AA"``. Returns
None when no variant matches the user rewrote the text.
"""
token = "{" + placeholder + "}"
for variant in _template_variants(text_en):
prefix, sep, suffix = variant.partition(token)
if not sep:
continue
if stored.startswith(prefix) and stored.endswith(suffix) and len(stored) > len(prefix) + len(suffix):
return stored[len(prefix) : len(stored) - len(suffix)]
return None
def retranslate_seeded_texts(hass: HomeAssistant, entry: ConfigEntry, lang: str) -> bool:
"""Bring untouched seeded texts into ``lang``. Returns True when changed.
Runs at every fleet-entry setup: cheap string comparisons, and idempotent
once the texts are in the current language they match that language's
variant and are rewritten to themselves.
The boot-time caller passes the SERVER language, so untouched seeded texts
converge on it the same convention notifications and digests follow,
because stored data is shared by every user of the instance. Seeding still
honours the caller's UI language for the immediate result; if the two
differ, the next reload converges.
"""
from ..templates import localize_template_text
changed = False
data = dict(entry.data)
def _localized(text_en: str) -> str:
return localize_template_text(text_en, lang) or text_en
obj = dict(data.get(CONF_OBJECT) or {})
if obj.get("name") in _template_variants("Battery Fleet") and obj.get("name") != _localized("Battery Fleet"):
obj["name"] = _localized("Battery Fleet")
data[CONF_OBJECT] = obj
changed = True
tasks = dict(data.get(CONF_TASKS) or {})
for task_id, task in tasks.items():
if not task.get(TASK_FLAG):
continue
new_task = dict(task)
if new_task.get("name") in _template_variants("Replace low batteries"):
new_task["name"] = _localized("Replace low batteries")
notes_en = (
"Aggregate battery check. The detail view lists which devices are low "
"and which battery types to buy."
)
if new_task.get("notes") in _template_variants(notes_en):
new_task["notes"] = _localized(notes_en)
if new_task != task:
tasks[task_id] = new_task
data[CONF_TASKS] = tasks
changed = True
parts = dict(data.get(CONF_PARTS) or {})
for part_id, part in parts.items():
if not str(part_id).startswith("batt_"):
continue
new_part = dict(part)
btype = _extract_placeholder(str(new_part.get("name") or ""), "{type} battery", "type")
if btype is not None:
new_part["name"] = (_localized("{type} battery")).format(type=btype)
months = _extract_placeholder(
str(new_part.get("notes") or ""), "Typical service life ~{months} months.", "months"
)
if months is not None:
new_part["notes"] = (_localized("Typical service life ~{months} months.")).format(months=months)
if new_part != part:
parts[part_id] = new_part
data[CONF_PARTS] = parts
changed = True
if changed:
title = (data.get(CONF_OBJECT) or {}).get("name") or entry.title
hass.config_entries.async_update_entry(entry, data=data, title=title)
return changed
def replaced_button_for(battery_plus_entity_id: str) -> str:
"""The Battery Notes 'replaced' button entity id for a battery_plus sensor.
@@ -22,5 +22,5 @@
"requirements": [
"pypdf>=4.3.0"
],
"version": "2.44.1"
"version": "2.46.0"
}
@@ -7,7 +7,14 @@ import logging
from pathlib import Path
from homeassistant.components import frontend, panel_custom
from homeassistant.components.http import StaticPathConfig
# HA 2026.8 moved StaticPathConfig into http/server.py and keeps a deliberate
# re-export in the package root, so this import is valid at runtime on 2026.7
# and 2026.8 alike. Strict mypy rejects re-exported names regardless; importing
# from .server would satisfy it but break every user still on 2026.7, which has
# no such module. Reads as unused under the older HA in the dev container —
# three other ignores in this codebase share that fate.
from homeassistant.components.http import StaticPathConfig # type: ignore[attr-defined]
from homeassistant.core import HomeAssistant
from .const import DOMAIN, PANEL_ICON, PANEL_NAME, PANEL_URL
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Broken spare-part links on {object_name}",
"description": "A task on **{object_name}** was just completed, but {count} spare-part links on it could not be resolved — the part, or the object that owns it, no longer exists.\n\nThe completion itself was recorded, so the work is not lost. What did not happen is the stock deduction: nothing was taken off any part's stock for this completion.\n\nTo fix it, open the task in the Maintenance panel and remove the stale part links, or point them at the parts you use now. Dismiss this notification once that is done."
},
"device_link_lost": {
"title": "{object} lost its device link",
"description": "**{object}** was attached to a Home Assistant device, so its maintenance entities appeared on that device's page. That device no longer exists — deleted, or its integration removed — and the object now shows a device of its own. Nothing else changed: its tasks and their history are untouched.\n\nPick a device again in the Maintenance panel under *Link to existing device*, or ignore this if an own device is fine. The stored link is deliberately kept, so the attachment resumes by itself if the device comes back."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Neplatné vazby na díly u {object_name}",
"description": "Úkol u objektu **{object_name}** byl právě dokončen, ale {count} vazeb na náhradní díly se nepodařilo vyhodnotit — díl nebo objekt, kterému patří, už neexistuje.\n\nSamotné dokončení bylo zaznamenáno, o práci tedy nepřijdete. Neproběhl jen odpis ze skladu: za toto dokončení se z žádného dílu nic neodečetlo.\n\nNáprava: otevřete úkol v panelu údržby a odstraňte zastaralé vazby na díly, nebo je nasměrujte na díly, které používáte teď. Po vyřízení toto oznámení zavřete."
},
"device_link_lost": {
"title": "{object} přišel o propojení se zařízením",
"description": "**{object}** byl propojen se zařízením Home Assistant, takže se jeho údržbové entity zobrazovaly na stránce tohoto zařízení. Zařízení už neexistuje — bylo smazáno nebo byla odebrána jeho integrace — a objekt nyní ukazuje vlastní zařízení. Nic dalšího se nemění: úkoly i jejich historie zůstávají nedotčené.\n\nVyberte zařízení znovu v panelu Údržba v části *Propojit s existujícím zařízením*, nebo toto upozornění ignorujte, pokud vlastní zařízení stačí. Uložené propojení je záměrně zachováno: vrátí-li se zařízení, propojení začne fungovat samo."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Ugyldige reservedelslinks på {object_name}",
"description": "En opgave på **{object_name}** blev netop udført, men {count} reservedelslinks kunne ikke slås op — delen, eller det objekt der ejer den, findes ikke længere.\n\nSelve udførelsen blev registreret, så arbejdet er ikke gået tabt. Det, der ikke skete, er lagertrækket: der blev ikke trukket noget fra nogen dels beholdning for denne udførelse.\n\nSådan retter du det: åbn opgaven i vedligeholdelsespanelet og fjern de forældede dellinks, eller peg dem mod de dele, du bruger nu. Luk denne notifikation, når det er gjort."
},
"device_link_lost": {
"title": "{object} mistede sin enhedstilknytning",
"description": "**{object}** var tilknyttet en Home Assistant-enhed, så dets vedligeholdelsesenheder blev vist på den enheds side. Den enhed findes ikke længere — slettet, eller dens integration fjernet — og objektet viser nu sin egen enhed. Intet andet ændrer sig: opgaverne og deres historik er urørte.\n\nVælg en enhed igen i vedligeholdelsespanelet under *Tilknyt eksisterende enhed*, eller se bort fra dette, hvis en egen enhed er fin. Den gemte tilknytning bevares med vilje: kommer enheden tilbage, virker tilknytningen af sig selv igen."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Ungültige Ersatzteil-Verknüpfungen bei {object_name}",
"description": "Eine Aufgabe bei **{object_name}** wurde gerade abgeschlossen, aber {count} Ersatzteil-Verknüpfungen ließen sich nicht auflösen — das Teil oder das Objekt, dem es gehört, existiert nicht mehr.\n\nDer Abschluss selbst wurde gespeichert, die Arbeit ist also nicht verloren. Nicht passiert ist die Bestandsbuchung: Für diesen Abschluss wurde von keinem Teil etwas abgezogen.\n\nZum Beheben: Öffne die Aufgabe im Wartungs-Panel und entferne die veralteten Teile-Verknüpfungen oder richte sie auf die Teile aus, die du jetzt nutzt. Schließe diese Meldung, sobald das erledigt ist."
},
"device_link_lost": {
"title": "{object} hat seine Geräteverknüpfung verloren",
"description": "**{object}** war mit einem Home-Assistant-Gerät verknüpft, sodass seine Wartungsentitäten auf dessen Geräteseite erschienen. Dieses Gerät existiert nicht mehr — gelöscht oder seine Integration entfernt — und das Objekt zeigt jetzt ein eigenes Gerät. Sonst ändert sich nichts: Aufgaben und Verlauf bleiben unberührt.\n\nWähle im Wartungs-Panel unter *Mit vorhandenem Gerät verknüpfen* erneut ein Gerät, oder ignoriere diesen Hinweis, wenn ein eigenes Gerät genügt. Die gespeicherte Verknüpfung bleibt absichtlich erhalten — kehrt das Gerät zurück, greift sie von selbst wieder."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Broken spare-part links on {object_name}",
"description": "A task on **{object_name}** was just completed, but {count} spare-part links on it could not be resolved — the part, or the object that owns it, no longer exists.\n\nThe completion itself was recorded, so the work is not lost. What did not happen is the stock deduction: nothing was taken off any part's stock for this completion.\n\nTo fix it, open the task in the Maintenance panel and remove the stale part links, or point them at the parts you use now. Dismiss this notification once that is done."
},
"device_link_lost": {
"title": "{object} lost its device link",
"description": "**{object}** was attached to a Home Assistant device, so its maintenance entities appeared on that device's page. That device no longer exists — deleted, or its integration removed — and the object now shows a device of its own. Nothing else changed: its tasks and their history are untouched.\n\nPick a device again in the Maintenance panel under *Link to existing device*, or ignore this if an own device is fine. The stored link is deliberately kept, so the attachment resumes by itself if the device comes back."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Enlaces de repuesto rotos en {object_name}",
"description": "Una tarea de **{object_name}** acaba de completarse, pero {count} enlaces a repuestos no se pudieron resolver — el repuesto, o el objeto al que pertenece, ya no existe.\n\nLa finalización sí se registró, así que el trabajo no se ha perdido. Lo que no ocurrió es el descuento de stock: no se restó nada del stock de ningún repuesto por esta finalización.\n\nPara arreglarlo: abre la tarea en el panel de Mantenimiento y elimina los enlaces obsoletos, o apúntalos a los repuestos que usas ahora. Descarta esta notificación cuando esté hecho."
},
"device_link_lost": {
"title": "{object} ha perdido su vínculo con el dispositivo",
"description": "**{object}** estaba vinculado a un dispositivo de Home Assistant, de modo que sus entidades de mantenimiento aparecían en la página de ese dispositivo. Ese dispositivo ya no existe — eliminado, o retirada su integración — y el objeto muestra ahora un dispositivo propio. Nada más cambia: sus tareas y su historial siguen intactos.\n\nElige de nuevo un dispositivo en el panel de Mantenimiento en *Vincular a un dispositivo existente*, o ignora este aviso si te vale un dispositivo propio. El vínculo guardado se conserva a propósito: si el dispositivo vuelve, la vinculación se restablece sola."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Rikkinäisiä varaosalinkkejä kohteessa {object_name}",
"description": "Kohteen **{object_name}** tehtävä merkittiin juuri tehdyksi, mutta {count} varaosalinkkiä ei voitu selvittää — osaa tai sen omistavaa kohdetta ei enää ole.\n\nItse suoritus kuitenkin tallennettiin, joten työ ei kadonnut. Tekemättä jäi varastosta vähentäminen: tästä suorituksesta ei vähennetty mitään minkään osan saldosta.\n\nKorjaaminen: avaa tehtävä huoltopaneelissa ja poista vanhentuneet osalinkit tai osoita ne niihin osiin, joita nyt käytät. Sulje tämä ilmoitus, kun asia on kunnossa."
},
"device_link_lost": {
"title": "{object} menetti laitelinkityksensä",
"description": "**{object}** oli linkitetty Home Assistant -laitteeseen, joten sen huoltoentiteetit näkyivät kyseisen laitteen sivulla. Laitetta ei enää ole — se poistettiin tai sen integraatio poistettiin — ja kohde näyttää nyt oman laitteensa. Muu ei muutu: tehtävät ja niiden historia säilyvät koskemattomina.\n\nValitse laite uudelleen Huolto-paneelissa kohdasta *Linkitä olemassa olevaan laitteeseen*, tai jätä tämä huomiotta, jos oma laite riittää. Tallennettu linkitys säilytetään tarkoituksella: jos laite palaa, linkitys toimii taas itsestään."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Liens de pièces invalides sur {object_name}",
"description": "Une tâche de **{object_name}** vient d'être terminée, mais {count} liens vers des pièces de rechange n'ont pas pu être résolus — la pièce, ou l'objet auquel elle appartient, n'existe plus.\n\nL'achèvement lui-même a bien été enregistré : le travail n'est pas perdu. Ce qui n'a pas eu lieu, c'est le décompte du stock : rien n'a été retiré du stock d'aucune pièce pour cet achèvement.\n\nPour corriger : ouvrez la tâche dans le panneau Maintenance et supprimez les liens de pièces obsolètes, ou faites-les pointer vers les pièces que vous utilisez aujourd'hui. Fermez cette notification une fois que c'est fait."
},
"device_link_lost": {
"title": "{object} a perdu son lien vers l'appareil",
"description": "**{object}** était rattaché à un appareil Home Assistant, si bien que ses entités de maintenance apparaissaient sur la page de cet appareil. Cet appareil n'existe plus — supprimé, ou son intégration retirée — et l'objet affiche désormais un appareil qui lui est propre. Rien d'autre ne change : ses tâches et leur historique sont intacts.\n\nChoisissez à nouveau un appareil dans le panneau Maintenance sous *Lier à un appareil existant*, ou ignorez ceci si un appareil propre convient. Le lien enregistré est volontairement conservé : si l'appareil revient, le rattachement reprend de lui-même."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "{object_name} पर टूटे हुए पार्ट-लिंक",
"description": "**{object_name}** का एक कार्य अभी पूरा किया गया, लेकिन उसके {count} स्पेयर-पार्ट लिंक हल नहीं हो सके — वह पार्ट, या उसका मालिक ऑब्जेक्ट, अब मौजूद नहीं है।\n\nपूर्ति स्वयं दर्ज हो गई है, इसलिए किया गया काम बेकार नहीं गया। जो नहीं हुआ वह है स्टॉक की कटौती: इस पूर्ति के लिए किसी भी पार्ट के स्टॉक से कुछ नहीं घटाया गया।\n\nठीक करने के लिए: रखरखाव पैनल में कार्य खोलें और पुराने पार्ट-लिंक हटा दें, या उन्हें उन पार्ट्स पर सेट करें जिनका आप अब उपयोग करते हैं। हो जाने पर इस सूचना को बंद कर दें।"
},
"device_link_lost": {
"title": "{object} का डिवाइस लिंक टूट गया",
"description": "**{object}** एक Home Assistant डिवाइस से जुड़ा था, इसलिए इसकी रखरखाव एंटिटी उसी डिवाइस के पेज पर दिखती थीं। वह डिवाइस अब मौजूद नहीं है — हटा दिया गया, या उसका इंटीग्रेशन निकाल दिया गया — और अब यह ऑब्जेक्ट अपना अलग डिवाइस दिखाता है। बाकी कुछ नहीं बदला: कार्य और उनका इतिहास वैसे ही हैं।\n\nरखरखाव पैनल में *मौजूदा डिवाइस से लिंक करें* के अंतर्गत दोबारा कोई डिवाइस चुनें, या अपना अलग डिवाइस ठीक लगे तो इसे अनदेखा करें। सहेजा गया लिंक जानबूझकर रखा गया है — डिवाइस लौटने पर यह अपने आप फिर काम करने लगेगा।"
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Hibás alkatrész-hivatkozások itt: {object_name}",
"description": "A(z) **{object_name}** egyik feladata épp elkészült, de {count} alkatrész-hivatkozást nem sikerült feloldani — az alkatrész vagy az azt birtokló objektum már nem létezik.\n\nMagát az elvégzést rögzítettük, tehát a munka nem veszett el. Ami elmaradt, az a készletcsökkentés: ehhez az elvégzéshez egyetlen alkatrész készletéből sem vontunk le semmit.\n\nA javításhoz nyissa meg a feladatot a karbantartási panelen, és távolítsa el az elavult alkatrész-hivatkozásokat, vagy irányítsa őket a most használt alkatrészekre. Ha kész, zárja be ezt az értesítést."
},
"device_link_lost": {
"title": "{object} elvesztette az eszközkapcsolatát",
"description": "**{object}** egy Home Assistant eszközhöz volt kapcsolva, így a karbantartási entitásai annak az eszköznek az oldalán jelentek meg. Az az eszköz már nem létezik — törölték, vagy eltávolították az integrációját — és az objektum most saját eszközt mutat. Más nem változik: a feladatok és az előzményeik érintetlenek.\n\nVálasszon újra eszközt a Karbantartás panelen a *Kapcsolás meglévő eszközhöz* alatt, vagy hagyja figyelmen kívül, ha a saját eszköz is megfelel. A mentett kapcsolat szándékosan megmarad: ha az eszköz visszatér, a kapcsolat magától újra él."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Collegamenti ai ricambi non validi su {object_name}",
"description": "Un'attività su **{object_name}** è appena stata completata, ma {count} collegamenti ai ricambi non sono stati risolti — il ricambio, o l'oggetto a cui appartiene, non esiste più.\n\nIl completamento è stato comunque registrato, quindi il lavoro non è perso. Ciò che non è avvenuto è lo scarico della scorta: per questo completamento non è stato sottratto nulla da nessun ricambio.\n\nPer sistemarlo: apri l'attività nel pannello Manutenzione ed elimina i collegamenti obsoleti, oppure indirizzali ai ricambi che usi ora. Chiudi questa notifica quando hai finito."
},
"device_link_lost": {
"title": "{object} ha perso il collegamento al dispositivo",
"description": "**{object}** era collegato a un dispositivo di Home Assistant, così le sue entità di manutenzione comparivano nella pagina di quel dispositivo. Quel dispositivo non esiste più — eliminato, o rimossa la sua integrazione — e l'oggetto ora mostra un dispositivo proprio. Nient'altro cambia: le attività e la loro cronologia restano intatte.\n\nScegli di nuovo un dispositivo nel pannello Manutenzione sotto *Collega a un dispositivo esistente*, oppure ignora questo avviso se un dispositivo proprio va bene. Il collegamento salvato viene mantenuto di proposito: se il dispositivo torna, il collegamento riprende da solo."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "{object_name} の部品リンクが壊れています",
"description": "**{object_name}** のタスクが完了しましたが、{count} 件の交換部品リンクを解決できませんでした。部品自体、またはその部品を持つ対象がすでに存在しません。\n\n完了自体は記録されているので、作業の記録は失われていません。行われなかったのは在庫の引き落としで、この完了ではどの部品の在庫も減っていません。\n\n修正するには、メンテナンスパネルでタスクを開き、古い部品リンクを削除するか、現在使っている部品に付け替えてください。対応が済んだら、この通知を閉じてください。"
},
"device_link_lost": {
"title": "{object} のデバイス連携が失われました",
"description": "**{object}** は Home Assistant のデバイスに連携していたため、メンテナンスのエンティティがそのデバイスのページに表示されていました。そのデバイスはもう存在しません(削除された、または統合が取り除かれた)ので、オブジェクトは自前のデバイスを表示しています。ほかは変わりません。タスクと履歴はそのままです。\n\nメンテナンスパネルの*既存のデバイスに連携*からデバイスを選び直すか、自前のデバイスで構わなければこの通知を無視してください。保存された連携先はあえて残してあるため、デバイスが戻れば連携も自動的に復帰します。"
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "{object_name}의 부품 연결이 끊어졌습니다",
"description": "**{object_name}**의 작업이 방금 완료되었지만, 부품 연결 {count}개를 확인할 수 없었습니다 — 부품 자체 또는 그 부품을 가진 객체가 더 이상 존재하지 않습니다.\n\n완료 기록 자체는 저장되었으므로 작업 내역은 사라지지 않았습니다. 이루어지지 않은 것은 재고 차감으로, 이번 완료에서는 어떤 부품의 재고도 줄지 않았습니다.\n\n해결하려면 유지보수 패널에서 작업을 열어 오래된 부품 연결을 삭제하거나 지금 사용하는 부품으로 다시 연결하세요. 처리한 뒤 이 알림을 닫으세요."
},
"device_link_lost": {
"title": "{object}의 기기 연결이 끊어졌습니다",
"description": "**{object}**은(는) Home Assistant 기기에 연결되어 있어 유지관리 엔터티가 해당 기기 페이지에 표시되었습니다. 그 기기는 더 이상 존재하지 않으며(삭제되었거나 통합이 제거됨) 이제 객체는 자체 기기를 표시합니다. 그 밖에는 달라진 것이 없습니다. 작업과 기록은 그대로입니다.\n\n유지관리 패널의 *기존 기기에 연결*에서 기기를 다시 선택하거나, 자체 기기로 충분하다면 이 알림을 무시하세요. 저장된 연결은 의도적으로 남겨 두므로 기기가 돌아오면 연결도 저절로 복구됩니다."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Ugyldige reservedelskoblinger på {object_name}",
"description": "En oppgave på **{object_name}** ble nettopp utført, men {count} reservedelskoblinger kunne ikke slås opp — delen, eller objektet som eier den, finnes ikke lenger.\n\nSelve utførelsen ble registrert, så arbeidet er ikke tapt. Det som ikke skjedde, er lagertrekket: ingenting ble trukket fra noen dels beholdning for denne utførelsen.\n\nSlik retter du det: åpne oppgaven i vedlikeholdspanelet og fjern de utdaterte delkoblingene, eller pek dem mot delene du bruker nå. Lukk dette varselet når det er gjort."
},
"device_link_lost": {
"title": "{object} mistet enhetskoblingen sin",
"description": "**{object}** var koblet til en Home Assistant-enhet, slik at vedlikeholdsentitetene lå på den enhetens side. Den enheten finnes ikke lenger — slettet, eller integrasjonen fjernet — og objektet viser nå sin egen enhet. Ellers er ingenting endret: oppgavene og historikken er urørt.\n\nVelg en enhet på nytt i vedlikeholdspanelet under *Koble til eksisterende enhet*, eller overse dette hvis en egen enhet holder. Den lagrede koblingen beholdes med vilje: kommer enheten tilbake, virker koblingen av seg selv igjen."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Ongeldige onderdeelkoppelingen bij {object_name}",
"description": "Een taak bij **{object_name}** is zojuist afgerond, maar {count} onderdeelkoppelingen konden niet worden opgezocht — het onderdeel, of het object waar het bij hoort, bestaat niet meer.\n\nDe afronding zelf is wel opgeslagen, dus het werk is niet verloren. Wat niet is gebeurd, is de voorraadafboeking: er is voor deze afronding niets van de voorraad afgehaald.\n\nOplossen doe je zo: open de taak in het onderhoudspaneel en verwijder de verouderde onderdeelkoppelingen, of laat ze verwijzen naar de onderdelen die je nu gebruikt. Sluit deze melding zodra dat gedaan is."
},
"device_link_lost": {
"title": "{object} is de apparaatkoppeling kwijt",
"description": "**{object}** was gekoppeld aan een Home Assistant-apparaat, zodat de onderhoudsentiteiten op die apparaatpagina stonden. Dat apparaat bestaat niet meer — verwijderd, of de integratie ervan weggehaald — en het object toont nu een eigen apparaat. Verder verandert er niets: taken en geschiedenis blijven ongemoeid.\n\nKies opnieuw een apparaat in het onderhoudspaneel onder *Koppelen aan bestaand apparaat*, of negeer dit als een eigen apparaat prima is. De opgeslagen koppeling blijft bewust bewaard, dus als het apparaat terugkomt werkt ze vanzelf weer."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Nieprawidłowe powiązania części w {object_name}",
"description": "Zadanie obiektu **{object_name}** zostało właśnie ukończone, ale {count} powiązań z częściami nie udało się rozwiązać — części albo obiektu, do którego należy, już nie ma.\n\nSamo ukończenie zostało zapisane, więc praca nie przepadła. Nie doszło natomiast do zdjęcia ze stanu: za to ukończenie nie odjęto niczego z zapasu żadnej części.\n\nAby to naprawić: otwórz zadanie w panelu konserwacji i usuń nieaktualne powiązania części albo wskaż nimi części, których używasz teraz. Zamknij to powiadomienie, gdy skończysz."
},
"device_link_lost": {
"title": "{object} utracił powiązanie z urządzeniem",
"description": "**{object}** był powiązany z urządzeniem Home Assistant, dzięki czemu jego encje konserwacji pojawiały się na stronie tego urządzenia. Tego urządzenia już nie ma — usunięto je albo usunięto jego integrację — i obiekt pokazuje teraz własne urządzenie. Poza tym nic się nie zmienia: zadania i ich historia pozostają nienaruszone.\n\nWybierz urządzenie ponownie w panelu Konserwacji w sekcji *Powiąż z istniejącym urządzeniem* albo zignoruj ten komunikat, jeśli własne urządzenie wystarczy. Zapisane powiązanie celowo pozostaje: gdy urządzenie wróci, powiązanie zadziała samo."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Vínculos de peça quebrados em {object_name}",
"description": "Uma tarefa de **{object_name}** acabou de ser concluída, mas {count} vínculos de peça não puderam ser resolvidos — a peça, ou o objeto dono dela, não existe mais.\n\nA conclusão em si foi registrada, então o trabalho não se perdeu. O que não aconteceu foi a baixa no estoque: nada foi descontado do estoque de nenhuma peça nesta conclusão.\n\nPara corrigir: abra a tarefa no painel de Manutenção e remova os vínculos obsoletos, ou aponte-os para as peças que você usa agora. Dispense esta notificação quando terminar."
},
"device_link_lost": {
"title": "{object} perdeu o vínculo com o dispositivo",
"description": "**{object}** estava vinculado a um dispositivo do Home Assistant, então suas entidades de manutenção apareciam na página daquele dispositivo. Esse dispositivo não existe mais — excluído, ou sua integração removida — e o objeto agora mostra um dispositivo próprio. Nada mais muda: as tarefas e o histórico permanecem intactos.\n\nEscolha um dispositivo novamente no painel de Manutenção em *Vincular a um dispositivo existente*, ou ignore este aviso se um dispositivo próprio servir. O vínculo salvo é mantido de propósito: se o dispositivo voltar, o vínculo volta a valer sozinho."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Ligações a peças inválidas em {object_name}",
"description": "Uma tarefa de **{object_name}** acabou de ser concluída, mas {count} ligações a peças não puderam ser resolvidas — a peça, ou o objeto a que pertence, já não existe.\n\nA conclusão em si foi registada, por isso o trabalho não se perdeu. O que não aconteceu foi a baixa de stock: nada foi descontado do stock de nenhuma peça nesta conclusão.\n\nPara corrigir: abra a tarefa no painel de Manutenção e remova as ligações obsoletas, ou aponte-as para as peças que usa agora. Dispense esta notificação quando estiver feito."
},
"device_link_lost": {
"title": "{object} perdeu a ligação ao dispositivo",
"description": "**{object}** estava ligado a um dispositivo do Home Assistant, pelo que as suas entidades de manutenção apareciam na página desse dispositivo. Esse dispositivo já não existe — apagado, ou removida a sua integração — e o objeto mostra agora um dispositivo próprio. Nada mais muda: as tarefas e o respetivo histórico ficam intactos.\n\nEscolha novamente um dispositivo no painel de Manutenção em *Ligar a um dispositivo existente*, ou ignore este aviso se um dispositivo próprio servir. A ligação guardada é mantida de propósito: se o dispositivo voltar, a ligação retoma sozinha."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Нерабочие ссылки на запчасти у {object_name}",
"description": "Задача объекта **{object_name}** только что выполнена, но {count} ссылок на запчасти не удалось разрешить — самой запчасти или объекта, которому она принадлежит, больше нет.\n\nСамо выполнение записано, так что работа не потеряна. Не произошло списание: за это выполнение с остатков запчастей ничего не списано.\n\nЧтобы исправить: откройте задачу на панели обслуживания и удалите устаревшие ссылки на запчасти либо переназначьте их на те запчасти, которыми вы пользуетесь сейчас. Закройте это уведомление, когда закончите."
},
"device_link_lost": {
"title": "{object} потерял привязку к устройству",
"description": "**{object}** был привязан к устройству Home Assistant, поэтому его сущности обслуживания отображались на странице этого устройства. Устройства больше нет — удалено или удалена его интеграция — и объект теперь показывает собственное устройство. Больше ничего не изменилось: задачи и их история не тронуты.\n\nВыберите устройство заново в панели обслуживания в разделе *Привязать к существующему устройству* или проигнорируйте это сообщение, если собственного устройства достаточно. Сохранённая привязка намеренно оставлена: если устройство вернётся, привязка заработает сама."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Trasiga reservdelskopplingar på {object_name}",
"description": "En uppgift på **{object_name}** slutfördes nyss, men {count} reservdelskopplingar kunde inte slås upp — delen, eller objektet som äger den, finns inte längre.\n\nSjälva slutförandet registrerades, så arbetet är inte förlorat. Det som inte hände är lageravdraget: ingenting drogs av från någon dels saldo för det här slutförandet.\n\nSå åtgärdar du det: öppna uppgiften i underhållspanelen och ta bort de föråldrade delkopplingarna, eller peka om dem till de delar du använder nu. Stäng den här aviseringen när det är klart."
},
"device_link_lost": {
"title": "{object} förlorade sin enhetskoppling",
"description": "**{object}** var kopplat till en Home Assistant-enhet, så dess underhållsentiteter visades på den enhetens sida. Den enheten finns inte längre — borttagen, eller dess integration avinstallerad — och objektet visar nu en egen enhet. Inget annat ändras: uppgifterna och deras historik är orörda.\n\nVälj en enhet igen i underhållspanelen under *Koppla till befintlig enhet*, eller strunta i detta om en egen enhet duger. Den sparade kopplingen behålls med flit: kommer enheten tillbaka återupptas kopplingen av sig själv."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "{object_name} üzerinde bozuk parça bağlantıları",
"description": "**{object_name}** üzerindeki bir görev az önce tamamlandı, ancak {count} yedek parça bağlantısı çözümlenemedi — parça ya da parçanın ait olduğu nesne artık mevcut değil.\n\nTamamlama kaydedildi, yani yapılan iş kaybolmadı. Gerçekleşmeyen şey stok düşümü: bu tamamlama için hiçbir parçanın stoğundan düşülmedi.\n\nDüzeltmek için: görevi Bakım panelinde açıp eski parça bağlantılarını kaldırın veya şu anda kullandığınız parçalara yönlendirin. İşiniz bitince bu bildirimi kapatın."
},
"device_link_lost": {
"title": "{object} cihaz bağlantısını kaybetti",
"description": "**{object}** bir Home Assistant cihazına bağlıydı; bu sayede bakım varlıkları o cihazın sayfasında görünüyordu. O cihaz artık yok — silindi ya da entegrasyonu kaldırıldı — ve nesne şimdi kendine ait bir cihaz gösteriyor. Başka bir şey değişmedi: görevler ve geçmişleri olduğu gibi duruyor.\n\nBakım panelinde *Mevcut cihaza bağla* altından yeniden bir cihaz seçin ya da kendi cihazı yeterliyse bunu yok sayın. Kayıtlı bağlantı bilerek korunuyor: cihaz geri gelirse bağlantı kendiliğinden yeniden çalışır."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "Непрацездатні посилання на запчастини в {object_name}",
"description": "Завдання об'єкта **{object_name}** щойно виконано, але {count} посилань на запчастини не вдалося розв'язати — самої запчастини або об'єкта, якому вона належить, більше немає.\n\nСаме виконання записано, тож роботу не втрачено. Не відбулося списання: за це виконання із залишків запчастин нічого не списано.\n\nЩоб виправити: відкрийте завдання на панелі обслуговування та видаліть застарілі посилання на запчастини або перенаправте їх на ті запчастини, якими ви користуєтеся зараз. Закрийте це сповіщення, коли завершите."
},
"device_link_lost": {
"title": "{object} втратив прив'язку до пристрою",
"description": "**{object}** було прив'язано до пристрою Home Assistant, тому його сутності обслуговування показувалися на сторінці цього пристрою. Пристрою більше немає — видалено або вилучено його інтеграцію — і об'єкт тепер показує власний пристрій. Більше нічого не змінилося: завдання та їхня історія лишилися недоторканими.\n\nОберіть пристрій знову в панелі обслуговування в розділі *Прив'язати до наявного пристрою* або проігноруйте це, якщо власного пристрою достатньо. Збережену прив'язку навмисно залишено: якщо пристрій повернеться, вона запрацює сама."
}
},
"exceptions": {
@@ -1822,6 +1822,10 @@
"broken_part_link": {
"title": "{object_name} 上的备件关联已失效",
"description": "**{object_name}** 上的一项任务刚刚完成,但其中 {count} 个备件关联无法解析——备件本身或拥有该备件的对象已不存在。\n\n完成记录本身已保存,因此这次工作没有白做。没有发生的是库存扣减:本次完成没有从任何备件的库存中扣除数量。\n\n修复方法:在维护面板中打开该任务,删除失效的备件关联,或将其重新指向你现在使用的备件。处理完成后关闭此通知。"
},
"device_link_lost": {
"title": "{object} 的设备关联已失效",
"description": "**{object}** 曾关联到一个 Home Assistant 设备,因此它的维护实体显示在该设备页面上。该设备已不存在——被删除,或其集成被移除——对象现在显示自己的设备。其他一切不变:任务及其历史记录未受影响。\n\n请在维护面板的*关联到现有设备*中重新选择一个设备,或者如果自有设备也可以,就忽略此提示。保存的关联被有意保留:设备若回来,关联会自动恢复。"
}
},
"exceptions": {
@@ -45,6 +45,12 @@ async def ws_battery_fleet_overview(hass: HomeAssistant, connection: websocket_a
"total": ov.total,
"low": ov.low,
"soon": ov.soon,
# The full roster, each row tagged low/soon/ok. The detail section
# lists it behind a disclosure so a device can be excluded before
# it ever goes low — previously the exclude control existed only on
# low rows, so a self-recharging vacuum could be dismissed only
# while it was already nagging (discussion #113).
"all": ov.all,
"needs_now": dict(ov.needs_now),
"needs_soon": dict(ov.needs_soon),
"types": ov.types,
+23 -23
View File
@@ -4,7 +4,7 @@
"state": "ON",
"led_brightness": 100,
"countdown_to_turn_off": 0,
"voltage": 121.2,
"voltage": 120.7,
"countdown_to_turn_on": 0,
"ac_frequency": 60,
"power_factor": 0.11,
@@ -22,15 +22,15 @@
},
"0xffffb40e0607af27": {
"state": "ON",
"voltage": 120.8,
"voltage": 120.7,
"ac_frequency": 60,
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"power": 2.2,
"power": 2.1,
"current": 0.12,
"energy": 24.11,
"power_factor": 0.15,
"power_factor": 0.14,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -45,9 +45,9 @@
"state": "ON",
"led_brightness": 100,
"countdown_to_turn_off": 0,
"voltage": 120.6,
"voltage": 120.3,
"countdown_to_turn_on": 0,
"energy": 44.22,
"energy": 44.26,
"power_factor": 0.89,
"ac_frequency": 60,
"update": {
@@ -58,7 +58,7 @@
"latest_release_notes": null
},
"linkquality": 83,
"power": 85,
"power": 83.2,
"current": 0.82,
"power_on_behavior": "on"
},
@@ -74,11 +74,11 @@
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"voltage": 120,
"voltage": 119.6,
"state": "ON",
"ac_frequency": 60,
"energy": 89.33,
"power": 100.8,
"energy": 89.39,
"power": 98,
"current": 0.89,
"power_factor": 0.94,
"update": {
@@ -96,12 +96,12 @@
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"voltage": 120.7,
"energy": 36.75,
"energy": 36.83,
"state": "ON",
"power": 93.4,
"current": 1.01,
"power": 69.8,
"current": 0.78,
"ac_frequency": 60,
"power_factor": 0.78,
"power_factor": 0.79,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -118,8 +118,8 @@
"ac_frequency": 60,
"energy": 5.74,
"current": 0.01,
"power": 0.1,
"power_factor": 0.11,
"power": 0,
"power_factor": 0,
"linkquality": 109,
"update": {
"state": "idle",
@@ -142,8 +142,8 @@
"state": "ON",
"current": 0.02,
"ac_frequency": 60,
"power": 0.4,
"power_factor": 0.2,
"power": 0.3,
"power_factor": 0.14,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -172,7 +172,7 @@
"0xffffb40e060893d8": {
"state": "ON",
"led_brightness": 100,
"voltage": 121.7,
"voltage": 121.2,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"energy": 2.98,
@@ -187,12 +187,12 @@
"latest_source": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V101_1.01.01.ota",
"latest_release_notes": null
},
"power_factor": 0.39,
"power": 1.7
"power_factor": 0.06,
"power": 0.1
},
"0xa4c1380d0679ffff": {
"battery": 100,
"temperature": 26.9,
"temperature": 27,
"temperature_units": "celsius",
"temperature_calibration": 0,
"update": {
@@ -245,7 +245,7 @@
},
"0xb40e060fffe717c5": {
"battery": 100,
"occupancy": true,
"occupancy": false,
"tamper": false,
"battery_low": false,
"linkquality": 65,