197 files

This commit is contained in:
Home Assistant Version Control
2026-08-21 18:53:37 +00:00
parent 89d9f5e49b
commit 383e9ed04e
197 changed files with 4386 additions and 1837 deletions
+1 -1
View File
@@ -1 +1 @@
{"pid": 70, "version": 1, "ha_version": "2026.8.2", "start_ts": 1787246176.3005776}
{"pid": 72, "version": 1, "ha_version": "2026.8.2", "start_ts": 1787338100.2662094}
+1 -1
View File
@@ -315,7 +315,7 @@
"type": "module"
},
{
"id": "ba07e776ada54bb899ab0dfd6f6f2dbd",
"id": "3dc0faac727c4411b93fbf34ae873574",
"url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1",
"type": "module"
}
@@ -53,6 +53,14 @@ class _OptionsFlowBase(TriggerConfigMixin, OptionsFlow):
def _save_new_task(self) -> ConfigFlowResult:
"""Save the current task and return to init."""
from .config_flow_schedule import build_new_task_record
from .const import MAX_TASKS_PER_OBJECT
# Same bound the WS create chokepoint enforces (drift audit 2026-08:
# this options-flow copy had no cap, so the flow could inflate
# ConfigEntry.data past the limit the API refuses).
if len(self.config_entry.data.get(CONF_TASKS, {})) >= MAX_TASKS_PER_OBJECT:
self._current_task = {}
return self._show_init_menu()
task_id = uuid4().hex
task_data = build_new_task_record(
@@ -681,6 +681,9 @@ class TriggerConfigMixin:
if to_state:
tc[CONF_TRIGGER_TO_STATE] = to_state
tc[CONF_TRIGGER_TARGET_CHANGES] = user_input.get(CONF_TRIGGER_TARGET_CHANGES, 1)
# #136: the new state must HOLD this long before a change counts
# (0 = count immediately — some sensors pulse only briefly).
tc[CONF_TRIGGER_FOR_MINUTES] = user_input.get(CONF_TRIGGER_FOR_MINUTES, 0)
_apply_recovery_flag(tc, user_input)
_apply_combinator(tc, user_input)
@@ -711,6 +714,9 @@ class TriggerConfigMixin:
mode=selector.NumberSelectorMode.BOX,
)
),
vol.Optional(CONF_TRIGGER_FOR_MINUTES, default=0): selector.NumberSelector(
selector.NumberSelectorConfig(min=0, max=1440, step=1, mode=selector.NumberSelectorMode.BOX)
),
**_recovery_field(self._current_task.get("trigger_config")),
}
schema_fields.update(_entity_logic_field(self._current_task.get("trigger_config", {}).get("entity_ids", [])))
@@ -5,7 +5,7 @@ from __future__ import annotations
import re
from enum import StrEnum
from homeassistant.const import Platform
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
DOMAIN = "maintenance_supporter"
@@ -341,11 +341,18 @@ CONF_PARTS = "parts"
# string literals in four files; find_fleet_entry, setup, the WS response
# and the fleet exclusion all key on them).
BATTERY_FLEET_OBJECT_FLAG = "battery_fleet"
# Marks the fleet's single aggregate task (detail view renders the battery
# section). Canonical home; helpers/battery_fleet_setup re-exports as TASK_FLAG.
BATTERY_FLEET_TASK_FLAG = "battery_fleet_task"
BATTERY_FLEET_EXCLUDED = "battery_fleet_excluded"
# #135: entity_ids manually ADDED to the fleet — batteries the discovery
# heuristics miss (no device_class, odd naming, self-charging filter). An
# include bypasses the heuristic and #107 filters, never the dedupe.
BATTERY_FLEET_INCLUDED = "battery_fleet_included"
# #135 follow-up: fleet-wide opt-in that keeps self-charging devices (phones,
# vacuums, smart rings — the #107 filter) in the roster as rechargeables, so
# "needs a recharge" can notify like "needs a swap". Default off: #107 stays.
BATTERY_FLEET_TRACK_SELF_CHARGING = "battery_fleet_track_self_charging"
# Task-side link: task["consumes_parts"] = [{"part_id", "quantity"}] — a
# completion decrements each linked part's stock.
CONF_TASK_CONSUMES_PARTS = "consumes_parts"
@@ -375,6 +382,7 @@ CONF_TASK_NFC_TAG = "nfc_tag_id"
# v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...).
CONF_TASK_READING_UNIT = "reading_unit"
MAX_READING_UNIT_LENGTH = 32
MAX_NFC_TAG_LENGTH = 256
CONF_TASK_INTERVAL_ANCHOR = "interval_anchor"
CONF_TASK_SCHEDULE_TIME = "schedule_time"
CONF_TASK_PRIORITY = "priority"
@@ -704,6 +712,11 @@ MAX_GROUP_TASK_REFS = 200
MAX_ID_LENGTH = 64 # entry_id, task_id, group_id (uuid hex = 32)
MAX_DATE_LENGTH = 20 # ISO 8601 date strings (e.g. 2026-04-21)
MAX_ENTITY_ID_LENGTH = 255 # HA entity_id max
# "No usable reading" state pair — 13 call sites hand-typed the tuple before
# the 2026-08 drift audit. Built from HA's own constants so it can never
# drift from core.
UNAVAILABLE_STATES = (STATE_UNAVAILABLE, STATE_UNKNOWN)
MAX_ENTITY_SLUG_LENGTH = 64 # task entity_slug
MAX_INTERVAL_DAYS = 3650 # 10 years — caps date arithmetic overflow
# Import payload byte caps. CSV is flat (one row per task) so 1 MB is ample;
@@ -42,6 +42,7 @@ from .const import (
SIGNAL_TASK_RESET,
STARTUP_GRACE_PERIOD_SECONDS,
TRIGGER_COMPLETION_COOLDOWN_SECONDS,
UNAVAILABLE_STATES,
HistoryEntryType,
MaintenanceStatus,
ScheduleType,
@@ -518,7 +519,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
issue_id = f"missing_trigger_{self.entry.entry_id}_{task_id}_{trigger_entity_id}"
state = self.hass.states.get(trigger_entity_id)
if state is not None and state.state not in ("unavailable", "unknown"):
if state is not None and state.state not in UNAVAILABLE_STATES:
# Entity exists and is available
self._entity_missing_refresh_count.pop(entity_key, None)
self._entity_unavailable_logged.pop(entity_key, None)
@@ -13,6 +13,7 @@ from .const import (
CONF_TASKS,
DOMAIN,
GLOBAL_UNIQUE_ID,
UNAVAILABLE_STATES,
MaintenanceStatus,
TriggerEntityState,
)
@@ -178,7 +179,7 @@ def _check_trigger_status(hass: HomeAssistant, data: Mapping[str, Any]) -> list[
if state is None:
entity_health = TriggerEntityState.MISSING
elif state.state in ("unavailable", "unknown"):
elif state.state in UNAVAILABLE_STATES:
entity_health = TriggerEntityState.UNAVAILABLE
else:
entity_health = TriggerEntityState.AVAILABLE
@@ -103,6 +103,11 @@ def _inject_per_entity_state(config: dict[str, Any], entity_state: dict[str, Any
elif trigger_type == TriggerType.STATE_CHANGE:
if "change_count" in entity_state:
config["trigger_change_count"] = entity_state["change_count"]
# #136: a hold window that was open when HA went down.
if "pending_since" in entity_state:
config["trigger_state_pending_since"] = entity_state["pending_since"]
if "pending_state" in entity_state:
config["trigger_state_pending_state"] = entity_state["pending_state"]
elif trigger_type == TriggerType.THRESHOLD:
tes = entity_state.get("threshold_exceeded_since")
if tes:
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
from ...const import (
EVENT_TRIGGER_ACTIVATED,
EVENT_TRIGGER_DEACTIVATED,
UNAVAILABLE_STATES,
)
_LOGGER = logging.getLogger(__name__)
@@ -80,7 +81,7 @@ class BaseTrigger(ABC):
self._unsub_listener = async_track_state_change_event(self.hass, [self.entity_id], self._handle_state_change_event)
# If state is unknown/unavailable, schedule a retry
if state.state in ("unavailable", "unknown"):
if state.state in UNAVAILABLE_STATES:
_LOGGER.info(
"Trigger entity %s is '%s' — will retry evaluation in 30s",
self.entity_id,
@@ -115,7 +116,7 @@ class BaseTrigger(ABC):
"""Re-check entity state after a delay."""
self._unsub_retry = None
state = self.hass.states.get(self.entity_id)
if state is None or state.state in ("unavailable", "unknown"):
if state is None or state.state in UNAVAILABLE_STATES:
_LOGGER.debug(
"Trigger entity %s still %s after retry",
self.entity_id,
@@ -176,7 +177,7 @@ class BaseTrigger(ABC):
# trigger entity is surfaced via the missing_trigger_entity repair
# flow instead. (Numeric-only triggers: any non-numeric value below
# is likewise ignored via the _get_numeric_value None check.)
if new_state.state in ("unavailable", "unknown"):
if new_state.state in UNAVAILABLE_STATES:
if not self._logged_unavailable:
_LOGGER.warning(
"Trigger entity %s became %s — keeping last trigger state",
@@ -24,6 +24,8 @@ from homeassistant.helpers.event import (
async_track_time_interval,
)
from ...const import UNAVAILABLE_STATES
if TYPE_CHECKING:
from ...sensor import MaintenanceSensor
from homeassistant.util import dt as dt_util
@@ -57,15 +59,15 @@ class RuntimeTrigger(BaseTrigger):
self._target_hours: float = trigger_config.get("trigger_runtime_hours", 100.0)
self._accumulated_seconds: float = trigger_config.get("trigger_accumulated_seconds", 0.0)
# Restore on_since timestamp for restart recovery
# Restore on_since timestamp for restart recovery. parse_persisted_utc
# coerces a naive legacy payload to UTC — dt_util.parse_datetime kept
# it naive here, and `utcnow() - naive` raises TypeError in the
# elapsed math (drift audit 2026-08; the sibling triggers coerced).
from ...helpers.dates import parse_persisted_utc
on_since_str = trigger_config.get("trigger_on_since")
self._on_since: str | None = None # ISO string stored for persistence
self._on_since_dt: datetime | None = None # parsed datetime for calculation
if on_since_str:
parsed = dt_util.parse_datetime(on_since_str)
if parsed:
self._on_since = on_since_str
self._on_since_dt = parsed
self._on_since_dt: datetime | None = parse_persisted_utc(on_since_str)
self._on_since: str | None = on_since_str if self._on_since_dt is not None else None
# Custom ON states (default: on, 1, true)
custom_on = trigger_config.get("trigger_on_states")
@@ -79,7 +81,7 @@ class RuntimeTrigger(BaseTrigger):
async def async_setup(self) -> None:
"""Set up runtime trigger with state restoration."""
state = self.hass.states.get(self.entity_id)
if state is None or state.state in ("unavailable", "unknown"):
if state is None or state.state in UNAVAILABLE_STATES:
# No USABLE state yet (#131 family): "unavailable" must not read
# as OFF — a running device whose sensor merely connects late
# would lose its restored on_since anchor and undercount. Keep
@@ -189,7 +191,7 @@ class RuntimeTrigger(BaseTrigger):
old_val = self._tracked_value(old_state)
# Handle unavailable/unknown — pause accumulation
if raw_state in ("unavailable", "unknown"):
if raw_state in UNAVAILABLE_STATES:
if self._on_since_dt is not None:
self._accumulate_elapsed()
self._on_since_dt = None
@@ -3,13 +3,18 @@
from __future__ import annotations
import logging
from datetime import datetime
from typing import TYPE_CHECKING, Any
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.event import (
EventStateChangedData,
async_call_later,
async_track_state_change_event,
)
from homeassistant.util import dt as dt_util
from ...const import UNAVAILABLE_STATES
if TYPE_CHECKING:
from ...sensor import MaintenanceSensor
@@ -34,6 +39,12 @@ class StateChangeTrigger(BaseTrigger):
# Setup saw no usable state -> reconcile on the first real one (#131).
# Class default so hand-built test instances inherit it.
_needs_latch_reconcile: bool = False
# #136 hold-window state — class defaults for the same reason.
_for_minutes: int = 0
_pending_state: str | None = None
_pending_since: str | None = None
_interrupted_pending: str | None = None
_timer_cancel: CALLBACK_TYPE | None = None
def __init__(
self,
@@ -58,6 +69,29 @@ class StateChangeTrigger(BaseTrigger):
self._last_state: str | None = None
self._needs_latch_reconcile = False
# #136: a transition only counts once the NEW state has HELD for this
# long. 0 (the default) counts immediately — deliberately, because some
# sensors express a real event only as a brief pulse; the filter is an
# opt-in for the flappy ones. Applies to BOTH modes: the single-shot
# alarm latch (target_changes == 1, the reporter's vacuum problem
# sensors glitching for seconds at night) and the cycle counter
# (a flicker is not a wash cycle).
self._for_minutes: int = int(trigger_config.get("trigger_for_minutes", 0) or 0)
self._timer_cancel: CALLBACK_TYPE | None = None
# A window cut short by an unavailability blip — the only case that
# may re-open on recovery (see _handle_state_transition).
self._interrupted_pending: str | None = None
# The state currently waiting out the hold window (None = no window).
self._pending_state: str | None = None
self._pending_since: str | None = None
# Persisted pending window from before a restart (consumed in setup).
from ...helpers.dates import parse_persisted_utc
self._restored_pending_state: str | None = trigger_config.get("trigger_state_pending_state")
self._restored_pending_dt: datetime | None = parse_persisted_utc(trigger_config.get("trigger_state_pending_since"))
if self._restored_pending_dt is None:
self._restored_pending_state = None
async def async_setup(self) -> None:
"""Set up state change trigger.
@@ -66,7 +100,7 @@ class StateChangeTrigger(BaseTrigger):
appears (old_state=None), so the trigger will self-heal automatically.
"""
state = self.hass.states.get(self.entity_id)
if state is None or state.state in ("unavailable", "unknown"):
if state is None or state.state in UNAVAILABLE_STATES:
# No USABLE state yet — the #131 family: trigger setup races both
# the entity's registration AND its device readiness (a Zigbee /
# Z-Wave problem sensor restores as unavailable long before it
@@ -85,6 +119,7 @@ class StateChangeTrigger(BaseTrigger):
self._last_state = state.state
self._reconcile_persisted_latch(state.state)
self._resume_pending_window(state.state)
# Register state change listener (override base: we handle events differently)
self._unsub_listener = async_track_state_change_event(self.hass, [self.entity_id], self._handle_state_transition)
@@ -119,8 +154,7 @@ class StateChangeTrigger(BaseTrigger):
self._change_count = 0
self._current_value = 0.0
self._triggered = False
if self.hass.is_running:
self.hass.async_create_task(self._persist_change_count())
self._persist_runtime_soon()
self.entity.async_update_trigger_state(
is_triggered=False,
current_value=0.0,
@@ -134,6 +168,113 @@ class StateChangeTrigger(BaseTrigger):
trigger_entity_id=self.entity_id,
)
def _resume_pending_window(self, live_state: str) -> None:
"""Resume (or discard) a hold window persisted before a restart (#136).
Mirrors the threshold trigger's exceeded-since recovery: the wall-clock
anchor survives the restart, so a state that kept holding through the
downtime commits immediately once the window has fully elapsed, and
otherwise the timer resumes with the remaining duration. A live state
that no longer matches the anchored one discards the window.
"""
restored_state, restored_dt = self._restored_pending_state, self._restored_pending_dt
self._restored_pending_state = None
self._restored_pending_dt = None
if self._for_minutes <= 0 or restored_dt is None or restored_state is None or self._triggered:
return
if _norm_state(live_state) != _norm_state(restored_state):
self._persist_runtime_soon()
return
elapsed = (dt_util.utcnow() - restored_dt).total_seconds()
if elapsed >= self._for_minutes * 60:
_LOGGER.debug(
"State hold recovery: elapsed %.0fs >= %ds, committing immediately: %s",
elapsed,
self._for_minutes * 60,
self.entity_id,
)
self._commit_transition(live_state, None)
return
self._pending_state = restored_state
self._pending_since = restored_dt.isoformat()
remaining = max(self._for_minutes * 60 - elapsed, 0)
_LOGGER.debug("State hold recovery: %.0fs remaining: %s", remaining, self.entity_id)
self._start_hold_timer(remaining_seconds=remaining)
def _start_pending(self, new_val: str) -> None:
"""(Re)open the hold window for *new_val* — commits when the timer fires."""
self._cancel_timer()
self._pending_state = new_val
self._pending_since = dt_util.utcnow().isoformat()
self._persist_runtime_soon()
self._start_hold_timer()
def _start_hold_timer(self, remaining_seconds: float | None = None) -> None:
self._cancel_timer()
duration = remaining_seconds if remaining_seconds is not None else self._for_minutes * 60
@callback
def _timer_fired(_now: datetime) -> None:
pending = self._pending_state
self._pending_state = None
self._pending_since = None
self._timer_cancel = None
if pending is None:
return
# Safety net: only commit while the state still holds.
live = self.hass.states.get(self.entity_id)
if live is None or _norm_state(live.state) != _norm_state(pending):
self._persist_runtime_soon()
return
_LOGGER.debug(
"State hold timer fired: %s held %r for %d min",
self.entity_id,
pending,
self._for_minutes,
)
self._commit_transition(pending, None)
self._timer_cancel = async_call_later(self.hass, duration, _timer_fired)
def _clear_pending(self) -> None:
"""Abandon the hold window (state moved on before it elapsed)."""
if self._pending_state is None and self._timer_cancel is None:
return
self._cancel_timer()
self._pending_state = None
self._pending_since = None
self._persist_runtime_soon()
def _cancel_timer(self) -> None:
if self._timer_cancel is not None:
self._timer_cancel()
self._timer_cancel = None
def _commit_transition(self, new_val: str, old_val: str | None) -> None:
"""Count one matching transition (immediately, or after its hold)."""
self._pending_state = None
self._pending_since = None
self._change_count += 1
self._current_value = float(self._change_count)
self._persist_runtime_soon()
_LOGGER.debug(
"State change counted: %s (%s -> %s) count=%d/%d",
self.entity_id,
old_val if old_val is not None else "<held>",
new_val,
self._change_count,
self._target_changes,
)
was_triggered = self._triggered
is_triggered = self._change_count >= self._target_changes
self._triggered = is_triggered
if is_triggered and not was_triggered:
self._on_trigger_activated(float(self._change_count))
elif not is_triggered and was_triggered:
self._on_trigger_deactivated(float(self._change_count))
@callback
def _handle_state_transition(self, event: Event[EventStateChangedData]) -> None:
"""Handle state transition and count matching changes."""
@@ -158,7 +299,7 @@ class StateChangeTrigger(BaseTrigger):
# reconcile the persisted latch against it (issue #131): when the
# entity restores AFTER our setup, this appearance is the first
# moment the latch can be checked against reality.
if new_val not in ("unavailable", "unknown"):
if new_val not in UNAVAILABLE_STATES:
self._needs_latch_reconcile = False
self._last_state = new_val
self._reconcile_persisted_latch(new_val)
@@ -167,7 +308,14 @@ class StateChangeTrigger(BaseTrigger):
old_val = old_state.state
# Handle unavailable/unknown with log-once pattern
if new_val in ("unavailable", "unknown"):
if new_val in UNAVAILABLE_STATES:
# #136: an unavailability blip is not "the state held" — abandon
# the hold window, but REMEMBER it: only a window that was
# actually running may re-open when the entity comes back (else a
# blip on a long-settled state would count a phantom transition).
if self._pending_state is not None:
self._interrupted_pending = self._pending_state
self._clear_pending()
if not self._logged_unavailable:
_LOGGER.warning(
"Trigger entity %s became %s",
@@ -185,6 +333,19 @@ class StateChangeTrigger(BaseTrigger):
new_val,
)
self._logged_unavailable = False
# #136: a window was running when the blip hit and the state came
# back unchanged — restart it (fresh clock; the normal transition
# path below cannot, because effective_old equals new_val here).
interrupted = self._interrupted_pending
self._interrupted_pending = None
if (
self._for_minutes > 0
and not self._triggered
and self._pending_state is None
and interrupted is not None
and _norm_state(new_val) == _norm_state(interrupted)
):
self._start_pending(new_val)
# First REAL state after a setup that saw none/unavailable (#131
# family): reconcile the persisted latch against it instead of
@@ -199,9 +360,16 @@ class StateChangeTrigger(BaseTrigger):
# Use _last_state as fallback when old_val is unavailable/unknown
effective_old = old_val
if old_val in ("unavailable", "unknown") and self._last_state is not None:
if old_val in UNAVAILABLE_STATES and self._last_state is not None:
effective_old = self._last_state
# #136: any real state movement means the previous state did NOT hold
# — abandon a running hold window (a matching transition right below
# opens a fresh one) and invalidate a blip-interruption marker.
if effective_old != new_val:
self._interrupted_pending = None
self._clear_pending()
# Check if transition matches pattern
matches = True
if self._from_state is not None and _norm_state(effective_old) != self._from_state:
@@ -210,28 +378,11 @@ class StateChangeTrigger(BaseTrigger):
matches = False
if matches and effective_old != new_val:
self._change_count += 1
self._current_value = float(self._change_count)
# Persist change count to survive restarts
if self.hass.is_running:
self.hass.async_create_task(self._persist_change_count())
_LOGGER.debug(
"State change counted: %s (%s -> %s) count=%d/%d",
self.entity_id,
old_val,
new_val,
self._change_count,
self._target_changes,
)
was_triggered = self._triggered
is_triggered = self._change_count >= self._target_changes
self._triggered = is_triggered
if is_triggered and not was_triggered:
self._on_trigger_activated(float(self._change_count))
elif not is_triggered and was_triggered:
self._on_trigger_deactivated(float(self._change_count))
if self._for_minutes > 0:
# #136: the transition only counts once new_val has held.
self._start_pending(new_val)
else:
self._commit_transition(new_val, old_val)
# Latch recovery: a single-shot state alarm (target_changes == 1 — an
# adopted problem sensor or an appliance event) clears when the entity
@@ -247,8 +398,7 @@ class StateChangeTrigger(BaseTrigger):
):
self._change_count = 0
self._current_value = 0.0
if self.hass.is_running:
self.hass.async_create_task(self._persist_change_count())
self._persist_runtime_soon()
self._triggered = False
self._on_trigger_deactivated(0.0)
@@ -268,19 +418,39 @@ class StateChangeTrigger(BaseTrigger):
"""Reset the change counter (after maintenance)."""
self._change_count = 0
self._current_value = 0.0
if self.hass.is_running:
self.hass.async_create_task(self._persist_change_count())
self._persist_runtime_soon()
_LOGGER.debug("State change counter reset: %s", self.entity_id)
async def _persist_change_count(self) -> None:
"""Persist change count to the Store for survival across restarts."""
def _persist_runtime_soon(self) -> None:
if self.hass.is_running:
self.hass.async_create_task(self._persist_runtime())
async def _persist_runtime(self) -> None:
"""Persist the full runtime dict (count + hold window) to the Store.
Always the COMPLETE dict: set_trigger_runtime replaces per-entity
state wholesale, so a partial write would drop the other half.
"""
data: dict[str, Any] = {"change_count": self._change_count}
if self._pending_since is not None and self._pending_state is not None:
data["pending_since"] = self._pending_since
data["pending_state"] = self._pending_state
await self._coordinator.async_persist_trigger_runtime(
self._task_id,
{"change_count": self._change_count},
data,
entity_id=self.entity_id,
)
async def async_teardown(self) -> None:
"""Clean up the hold timer on teardown."""
self._cancel_timer()
await super().async_teardown()
def reset(self) -> None:
"""Reset trigger and counter."""
"""Reset trigger, counter and any running hold window."""
super().reset()
self._cancel_timer()
self._pending_state = None
self._pending_since = None
self._interrupted_pending = None
self.reset_count()
@@ -48,22 +48,11 @@ class ThresholdTrigger(BaseTrigger):
self._timer_cancel: CALLBACK_TYPE | None = None
# Restore persisted exceeded-since timestamp (survives HA restarts)
exceeded_since = trigger_config.get("trigger_threshold_exceeded_since")
self._exceeded_since: str | None = None
self._exceeded_since_dt: datetime | None = None
if exceeded_since:
try:
parsed = datetime.fromisoformat(exceeded_since)
# Older payloads may be naive — assume UTC since live writes
# use dt_util.utcnow().isoformat() (TZ-aware).
if parsed.tzinfo is None:
from datetime import UTC
from ...helpers.dates import parse_persisted_utc
parsed = parsed.replace(tzinfo=UTC)
self._exceeded_since_dt = parsed
self._exceeded_since = exceeded_since
except (ValueError, TypeError):
pass
exceeded_since = trigger_config.get("trigger_threshold_exceeded_since")
self._exceeded_since_dt: datetime | None = parse_persisted_utc(exceeded_since)
self._exceeded_since: str | None = exceeded_since if self._exceeded_since_dt is not None else None
def _value_exceeds_threshold(self, value: float) -> bool:
"""Check if the value exceeds configured thresholds."""
@@ -62,3 +62,35 @@ describe("adopt-problem-sensors dialog: suggested part", () => {
expect(byId["binary_sensor.pump_problem"].part_id).to.equal(undefined);
});
});
describe("adopt-problem-sensors dialog: flicker filter (#136)", () => {
it("defaults to 0 and then omits for_minutes from the payload", async () => {
const { el, sent } = await mountOpen();
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".for-input")!;
expect(input, "the for-minutes field renders").to.exist;
expect(input.value).to.equal("0");
el.shadowRoot!.querySelectorAll<HTMLElement>("ha-button")[1].click();
await el.updateComplete;
await new Promise((r) => setTimeout(r, 0));
const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as {
selections: Array<{ for_minutes?: number }>;
};
expect(adopt.selections.every((s) => s.for_minutes === undefined)).to.equal(true);
});
it("forwards the entered minutes on every selection", async () => {
const { el, sent } = await mountOpen();
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".for-input")!;
input.value = "10";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
el.shadowRoot!.querySelectorAll<HTMLElement>("ha-button")[1].click();
await el.updateComplete;
await new Promise((r) => setTimeout(r, 0));
const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as {
selections: Array<{ for_minutes?: number }>;
};
expect(adopt.selections.length).to.be.greaterThan(0);
expect(adopt.selections.every((s) => s.for_minutes === 10)).to.equal(true);
});
});
@@ -13,7 +13,7 @@
* the no-fetch fast paths of ensureLocale/isLocaleLoaded.
*/
import { expect } from "@open-wc/testing";
import { t, isLocaleLoaded, ensureLocale } from "../styles";
import { t, isLocaleLoaded, ensureLocale, seedEnglish } from "../styles";
describe("i18n runtime loader", () => {
it("serves bundled English synchronously", () => {
@@ -60,4 +60,25 @@ describe("i18n runtime loader", () => {
delete g!.store.pt;
}
});
it("seedEnglish MERGES so a stale first-loader cannot shadow newer keys (#135 regression)", () => {
// After an update, a cached app shell can load an OLD card bundle first;
// its EN table lacks every key the fresh panel introduced. First-wins
// seeding froze that stale table and the panel rendered raw keys
// ("BATTERY_FLEET_ADD"). The merge keeps existing entries (steady state
// unchanged) but lets every bundle contribute the keys it knows.
const g = (window as unknown as {
__msLocales: { store: Record<string, Record<string, string>> };
}).__msLocales;
const original = g.store.en;
try {
// Simulate the stale bundle having seeded first with a tiny old table.
g.store.en = { loading: "Old loading text" };
seedEnglish({ loading: "Fresh loading text", brand_new_key: "New feature label" });
expect(t("loading", "en"), "existing entries win conflicts").to.equal("Old loading text");
expect(t("brand_new_key", "en"), "newer keys are contributed, never raw").to.equal("New feature label");
} finally {
g.store.en = original;
}
});
});
@@ -162,6 +162,23 @@ describe("task-dialog trigger_config roundtrip closure (#103 class)", () => {
});
});
it("state_change: the hold filter (for_minutes, #136) survives the roundtrip", async () => {
const tc = await saveRoundtrip({
type: "state_change",
entity_id: "binary_sensor.vacuum_problem",
entity_ids: ["binary_sensor.vacuum_problem"],
trigger_to_state: "on",
trigger_target_changes: 1,
trigger_for_minutes: 10,
auto_complete_on_recovery: true,
});
expect(tc).to.deep.include({
type: "state_change",
trigger_to_state: "on",
trigger_for_minutes: 10,
});
});
it("compound: per-condition attribute/baseline/entity_logic survive via carry", async () => {
const tc = await saveRoundtrip({
type: "compound",
@@ -122,6 +122,22 @@ describe("task-documents", () => {
window.open = orig;
}
});
it("refuses to window.open a non-http(s) weblink (defense-in-depth, XSS)", async () => {
const EVIL = { id: "d4", kind: "weblink", title: "Evil", url: "javascript:alert(document.cookie)", tags: [], task_ids: ["t1"] };
const orig = window.open;
let opened = "NOT_CALLED";
window.open = ((u: string) => { opened = u; return null; }) as typeof window.open;
try {
const { el } = await mount(true, [EVIL]);
const info = el.shadowRoot!.querySelector<HTMLElement>(".tdoc-row .tdoc-info")!;
info.click();
await el.updateComplete;
expect(opened, "a javascript: weblink must never reach window.open").to.equal("NOT_CALLED");
} finally {
window.open = orig;
}
});
});
// ── part mode (v2.26): same component, linking via part_ids ─────────────────
@@ -50,6 +50,9 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
@state() private _selected: Set<string> = new Set();
@state() private _users: HAUser[] = [];
@state() private _responsible = "";
// #136: minutes the problem must persist before the created task triggers
// (one field applied to every selection). "0" = react to the first flicker.
@state() private _forMinutes = "0";
private _localeReady = false;
private _userService: UserService | null = null;
@@ -72,6 +75,7 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
this._sensors = [];
this._selected = new Set();
this._responsible = "";
this._forMinutes = "0";
try {
if (!this._userService) this._userService = new UserService(this.hass);
else this._userService.updateHass(this.hass);
@@ -126,6 +130,7 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
device_id: s.device_id ?? undefined,
part_id: s.suggested_part_id ?? undefined,
responsible_user_id: this._responsible || undefined,
for_minutes: parseInt(this._forMinutes, 10) > 0 ? parseInt(this._forMinutes, 10) : undefined,
}));
const result = await this.hass.connection.sendMessagePromise<AdoptResponse>({
type: "maintenance_supporter/problem_sensors/adopt",
@@ -214,6 +219,23 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
</div>
`}
${!this._loading && this._sensors.length > 0
? html`
<label class="responsible">
<span>${t("for_at_least_minutes", L)}</span>
<input
class="for-input"
type="number"
min="0"
max="1440"
.value=${this._forMinutes}
@input=${(e: Event) => (this._forMinutes = (e.target as HTMLInputElement).value)}
/>
</label>
<div class="for-hint">${t("adopt_for_minutes_hint", L)}</div>
`
: nothing}
${!this._loading && this._sensors.length > 0 && this._users.length > 0
? html`
<label class="responsible">
@@ -382,6 +404,19 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
color: var(--secondary-text-color);
flex-wrap: wrap;
}
.for-input {
width: 70px;
background: var(--card-background-color);
color: var(--primary-text-color);
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 4px 6px;
}
.for-hint {
font-size: 11px;
color: var(--secondary-text-color);
margin: -4px 0 2px;
}
.responsible select {
flex: 1;
min-width: 140px;
@@ -66,11 +66,17 @@ interface Overview {
needs_soon: Record<string, number>;
types: string[];
excluded?: { entity_id: string; device_name: string }[];
// #135 follow-up: fleet-wide "track self-charging devices" opt-in.
track_self_charging?: boolean;
}
export class MaintenanceBatteryFleetSection extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
/** Flat mode drops the section's own card chrome (border/background) —
* set by the Lovelace card wrapper, whose ha-card provides the chrome. */
@property({ type: Boolean }) public flat = false;
@state() private _ov: Overview | null = null;
@state() private _loading = false;
@state() private _marking = false;
@@ -198,6 +204,26 @@ export class MaintenanceBatteryFleetSection extends LitElement {
}
}
// #135 follow-up: fleet-wide opt-in that keeps self-charging devices
// (phones, vacuums, smart rings) in the roster as rechargeables.
private async _setTrackSelf(e: Event): Promise<void> {
const enabled = (e.target as HTMLInputElement).checked;
if (this._marking) return;
this._marking = true;
this._error = "";
try {
await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/battery_fleet/set_track_self_charging",
enabled,
});
await this._load();
} catch (e2) {
this._error = describeWsError(e2, this._lang);
} finally {
this._marking = false;
}
}
/** Lazy: the recorder-backed history is fetched once, when the roster is
* first expanded — most panel visits never open it. */
private _loadHistory = async (e: Event): Promise<void> => {
@@ -517,6 +543,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
></ha-selector>
<div class="bf-roster-hint">${t("battery_fleet_add_hint", L)}</div>
</div>
<label class="bf-track-self">
<input
type="checkbox"
.checked=${!!ov.track_self_charging}
.disabled=${this._marking}
@change=${this._setTrackSelf}
/>
${t("battery_fleet_track_self", L)}
</label>
<div class="bf-roster-hint">${t("battery_fleet_track_self_hint", L)}</div>
</details>
`
: nothing}
@@ -558,6 +594,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
flex-direction: column;
gap: 10px;
}
:host([flat]) .bf-card {
background: transparent;
border: none;
border-radius: 0;
margin: 0;
padding: 0;
}
.bf-head {
display: flex;
align-items: center;
@@ -614,22 +657,69 @@ export class MaintenanceBatteryFleetSection extends LitElement {
.bf-list {
font-weight: 500;
}
/* Cross-row column alignment (same subgrid pattern as the task table,
* issue 66): the LIST owns one column template, every row spans it via
* subgrid, and each element is PINNED to its column below - so an
* optional element (sparkline, percentage, forecast date) leaves its
* column empty instead of letting the rest of the row drift. max-content
* columns collapse to 0 when a whole list never fills them (the low list
* has no status chip, no sparkline, no date). */
.bf-rows {
display: flex;
flex-direction: column;
gap: 4px;
display: grid;
grid-template-columns: minmax(0, 1fr) repeat(9, max-content);
column-gap: 8px;
}
.bf-row {
display: flex;
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
align-items: center;
gap: 10px;
padding: 6px 0;
border-bottom: 1px solid var(--divider-color);
}
/* Column pinning: 1 name, 2 status/offline chip, 3 type, 4 charging
* icon, 5 sparkline, 6 level bar, 7 percentage, 8 row action
* (mark-one / record-swap), 9 forecast date, 10 exclude eye. */
.bf-dev {
flex: 1;
grid-column: 1;
min-width: 0;
}
.bf-offline,
.bf-status {
grid-column: 2;
justify-self: end;
}
.bf-type {
grid-column: 3;
}
.bf-recharge {
grid-column: 4;
}
.bf-spark {
grid-column: 5;
}
.bf-bar {
grid-column: 6;
}
.bf-level {
grid-column: 7;
justify-self: end;
}
.bf-row .bf-mark {
grid-column: 8;
}
.bf-predicted {
grid-column: 9;
justify-self: end;
}
.bf-row .bf-mark.bf-exclude {
grid-column: 10;
}
.bf-offline {
color: var(--secondary-text-color);
font-size: 12px;
font-style: italic;
}
.bf-type {
color: var(--secondary-text-color);
font-size: 13px;
@@ -648,13 +738,48 @@ export class MaintenanceBatteryFleetSection extends LitElement {
flex: 0 0 auto;
cursor: help;
}
/* On phones the row cannot fit name + chips + curve + bar + date: the
* decorations yield (the percentage still carries the number). */
/* On phones the row cannot fit name + chips + curve + bar + date in ONE
* line: the decorations yield (the percentage still carries the number)
* and the row wraps to two lines - the name spans the full width, the
* status chip moves under it (left, into the name column) and the rest
* keeps its pinned subgrid column, so type / percentage / date / eye
* stay aligned across rows. Without this the fixed max-content columns
* overflowed 400 px and the chips overlapped the wrapped names. */
@media (max-width: 640px) {
.bf-spark,
.bf-bar {
display: none;
}
.bf-row {
row-gap: 2px;
}
.bf-dev {
grid-column: 1 / 9;
grid-row: 1;
}
.bf-offline,
.bf-status {
/* Line 1, RIGHT - over the date+eye columns, which are wide enough
* for any chip. Pinning the chip into the 1fr rest column instead
* overlapped the type text (the rest column shrinks below chip
* width, and a span onto an fr track never grows fixed tracks). */
grid-column: 9 / 11;
grid-row: 1;
justify-self: end;
}
.bf-type {
grid-row: 2;
max-width: 44vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bf-recharge,
.bf-level,
.bf-predicted,
.bf-row .bf-mark {
grid-row: 2;
}
}
.bf-spark-line {
fill: none;
@@ -834,6 +959,18 @@ export class MaintenanceBatteryFleetSection extends LitElement {
border-radius: 10px;
padding: 1px 4px 1px 10px;
}
.bf-track-self {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 13px;
cursor: pointer;
}
.bf-track-self input {
accent-color: var(--primary-color);
margin: 0;
}
`;
}
@@ -3,7 +3,7 @@
import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { HomeAssistant } from "../types";
import { t, nativeFieldStyles } from "../styles";
import { langOf, t, nativeFieldStyles } from "../styles";
export interface ConfirmOptions {
title: string;
@@ -90,7 +90,7 @@ export class MaintenanceConfirmDialog extends LitElement {
render() {
if (!this._open) return nothing;
const lang = this.hass?.language || "en";
const lang = langOf(this.hass);
return html`
<ha-dialog open @closed=${this._cancel}>
<div class="dialog-title">${this._title}</div>
@@ -15,7 +15,7 @@ import { describeWsError } from "../ws-errors";
import { downloadUrl } from "../helpers/download";
import { downloadSignedDocument, openSignedDocument, signDocumentPath } from "../helpers/document-url";
import { formatBytes } from "../helpers/format-bytes";
import { CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
import type { HomeAssistant } from "../types";
interface MaintenanceDocument {
@@ -263,7 +263,7 @@ export class MaintenanceDocumentsSection extends LitElement {
}
private async _delete(doc: MaintenanceDocument): Promise<void> {
const name = doc.title || doc.filename || doc.url || "";
const name = docDisplayName(doc);
if (!window.confirm(t("doc_delete_confirm", this._lang).replace("{name}", name))) return;
this._busy = true;
this._error = "";
@@ -447,7 +447,7 @@ export class MaintenanceDocumentsSection extends LitElement {
}
}}
>
<div class="doc-title">${doc.title || doc.filename || doc.url}</div>
<div class="doc-title">${docDisplayName(doc)}</div>
<div class="doc-meta">${meta}</div>
</div>
<div class="doc-row-actions">
@@ -8,6 +8,7 @@ import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { HomeAssistant } from "../types";
import { t } from "../styles";
import { downloadTextFile } from "../helpers/download";
interface QrResult {
svg_data_uri: string;
@@ -194,16 +195,13 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
const svgContent = decodeURIComponent(
result.svg_data_uri.replace("data:image/svg+xml,", ""),
);
const blob = new Blob([svgContent], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const name = this._taskName
? `${this._objectName}-${this._taskName}`
: this._objectName;
a.download = `qr-${sanitizeFilename(name)}-${suffix}.svg`;
a.click();
URL.revokeObjectURL(url);
// helpers/download.ts, not a hand-rolled anchor: the detached-anchor +
// immediate-revoke pattern this used silently fails in the Companion
// WebView (drift audit 2026-08 caught the bypass).
downloadTextFile(svgContent, `qr-${sanitizeFilename(name)}-${suffix}.svg`, "image/svg+xml");
}
private _close(): void {
@@ -7,10 +7,12 @@
*/
import { LitElement, html, css, nothing } from "lit";
import { docDisplayName } from "../helpers/document-categories";
import { property, state } from "lit/decorators.js";
import { t, ensureLocale, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
import { openSignedDocument } from "../helpers/document-url";
import { isSafeHttpUrl } from "../helpers/url";
import { formatBytes } from "../helpers/format-bytes";
import type { HomeAssistant } from "../types";
@@ -135,7 +137,10 @@ export class MaintenanceStorageSectionCard extends LitElement {
private async _openResult(doc: SearchResult): Promise<void> {
if (doc.kind === "weblink") {
window.open(doc.url, "_blank", "noopener");
// Defense-in-depth (mirrors task-documents/documents-section): never
// hand a non-http(s) weblink to window.open, even though the WS add
// path already rejects such schemes.
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
return;
}
try {
@@ -150,7 +155,7 @@ export class MaintenanceStorageSectionCard extends LitElement {
<div class="obj-row result-row" title=${t("doc_open", L)} @click=${() => this._openResult(doc)}>
<ha-icon icon=${doc.kind === "weblink" ? "mdi:link-variant" : "mdi:file-document-outline"}></ha-icon>
<div class="result-info">
<div class="result-title">${doc.title || doc.filename || doc.url}</div>
<div class="result-title">${docDisplayName(doc)}</div>
<div class="result-obj">${doc.object_name}</div>
</div>
<ha-icon class="result-open" icon=${doc.kind === "weblink" ? "mdi:open-in-new" : "mdi:eye-outline"}></ha-icon>
@@ -1149,6 +1149,8 @@ export class MaintenanceTaskDialog extends LitElement {
if (this._triggerFromState) triggerConfig.trigger_from_state = this._triggerFromState;
if (this._triggerToState) triggerConfig.trigger_to_state = this._triggerToState;
if (this._triggerTargetChanges) { const v = parseInt(this._triggerTargetChanges, 10); if (!isNaN(v)) triggerConfig.trigger_target_changes = v; }
// #136: the new state must hold this long before a change counts.
if (this._triggerForMinutes) { const v = parseInt(this._triggerForMinutes, 10); if (!isNaN(v)) triggerConfig.trigger_for_minutes = v; }
} else if (this._triggerType === "runtime") {
if (this._triggerRuntimeHours) { const v = parseFloat(this._triggerRuntimeHours); if (!isNaN(v)) triggerConfig.trigger_runtime_hours = v; }
const onStates = this._triggerOnStates.split(",").map((s) => s.trim()).filter(Boolean);
@@ -2246,6 +2248,14 @@ export class MaintenanceTaskDialog extends LitElement {
@input=${(e: Event) => (this._triggerTargetChanges = (e.target as HTMLInputElement).value)}
></ms-textfield>
<div class="field-help">${t("target_changes_help", L)}</div>
<ms-textfield
label="${t("for_at_least_minutes", L)}"
type="number"
min="0"
.value=${this._triggerForMinutes}
@input=${(e: Event) => (this._triggerForMinutes = (e.target as HTMLInputElement).value)}
></ms-textfield>
<div class="field-help">${t("for_minutes_state_help", L)}</div>
`;
}
if (this._triggerType === "runtime") {
@@ -15,8 +15,9 @@ import { t, ensureLocale, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
import { downloadUrl } from "../helpers/download";
import { downloadSignedDocument, openSignedDocument } from "../helpers/document-url";
import { isSafeHttpUrl } from "../helpers/url";
import { formatBytes } from "../helpers/format-bytes";
import { CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
import type { HomeAssistant } from "../types";
interface Doc {
@@ -144,7 +145,11 @@ export class MaintenanceTaskDocuments extends LitElement {
private async _open(doc: Doc): Promise<void> {
if (doc.kind === "weblink") {
window.open(doc.url, "_blank", "noopener");
// Defense-in-depth: the add-link WS path already refuses non-http(s)
// schemes, but a weblink stored before that check (or via a future
// path) must never reach window.open as javascript:/data: — guard the
// sink too, like documents-section does.
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
return;
}
// A per-task page hint jumps straight to the relevant page via the PDF
@@ -206,7 +211,7 @@ export class MaintenanceTaskDocuments extends LitElement {
>
<option value="" ?selected=${!this._attachId}>${t("doc_link_existing", L)}</option>
${available.map(
(d) => html`<option value=${d.id} ?selected=${d.id === this._attachId}>${d.title || d.filename || d.url}</option>`,
(d) => html`<option value=${d.id} ?selected=${d.id === this._attachId}>${docDisplayName(d)}</option>`,
)}
</select>
<button class="tdoc-btn" ?disabled=${this._busy || !this._attachId} @click=${this._link}>
@@ -240,7 +245,7 @@ export class MaintenanceTaskDocuments extends LitElement {
}
}}
>
<div class="tdoc-title">${doc.title || doc.filename || doc.url}</div>
<div class="tdoc-title">${docDisplayName(doc)}</div>
<div class="tdoc-meta">
${meta}${page ? html` · <span class="tdoc-pagetag">${t("doc_page", L)} ${page}</span>` : nothing}
</div>
@@ -44,7 +44,7 @@ import type { MaintenanceQrDialog } from "./components/qr-dialog";
import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-actions-dialog";
import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog";
import type { HomeAssistant, MaintenanceObject } from "./types";
import { ensureLocale, isLocaleLoaded } from "./styles";
import { ensureLocale, isLocaleLoaded, langOf } from "./styles";
const OBJECT_DIALOG_TAG = "maintenance-object-dialog";
const TASK_DIALOG_TAG = "maintenance-task-dialog";
@@ -91,7 +91,7 @@ function syncHass(el: HTMLElement & { hass?: HomeAssistant }): boolean {
// in English even when the rest of HA is localized. Fetch the user's locale
// (idempotent + cached) and force a re-render once it's in, so the first paint
// (which may be English) is corrected to the user's language.
const lang = hass.language || "en";
const lang = langOf(hass);
if (!isLocaleLoaded(lang)) {
void ensureLocale(lang).then(() => {
(el as unknown as { requestUpdate?: () => void }).requestUpdate?.();
@@ -279,6 +279,10 @@ const HISTORY_TYPE_TO_STATUS: Record<string, string> = {
completed: "ok",
reset: "ok",
skipped: "due_soon",
// A cycle that was NOT done in time (skip-while-overdue writes it, and the
// coordinator stamps it automatically) — must never paint green. Found by
// the 2026-08 drift audit: the missing entry fell through to "ok".
missed: "overdue",
triggered: "triggered",
trigger_replaced: "triggered",
trigger_removed: "ok", // config change, not a due/triggered event
@@ -517,6 +517,7 @@
"group_by_label": "Seskupit podle",
"state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.",
"target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).",
"for_minutes_state_help": "0 počítá každou změnu ihned. S minutami musí nový stav nejprve vydržet — krátké bliknutí pak nespustí ani se nepočítá.",
"qr_print_title": "Tisk QR kódů",
"qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.",
"qr_print_load": "Načíst objekty",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Nebylo potřeba",
"quick_complete_success": "Rychle označeno jako hotové",
"trigger_replaced": "Spouštěč nahrazen",
"trigger_removed": "Spouštěč odebrán",
"add": "Přidat",
"show_stats": "Zobrazit statistiky + grafy",
"hide_stats": "Skrýt statistiky",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Za předpokladu včasného dokončení.",
"schedule_preview_ends": "(série končí)",
"adopt_problem_responsible": "Odpovědná osoba pro všechny převzaté úkoly (volitelné)",
"adopt_for_minutes_hint": "Spustit až když problém trvá takto dlouho — 0 reaguje na první bliknutí.",
"adopt_problem_configure": "Konfigurovat",
"history_auto": "Automaticky",
"battery_fleet_title": "Flotila baterií",
@@ -838,6 +841,8 @@
"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_add": "Přidat baterii",
"battery_fleet_add_hint": "Vyberte senzor baterie, který automatická detekce přehlédla — okamžitě se objeví v seznamu.",
"battery_fleet_track_self": "Sledovat samodobíjecí baterie",
"battery_fleet_track_self_hint": "Telefony, vysavače a další samodobíjecí zařízení se zobrazují jako akumulátory — při nízkém stavu žádají o nabití, nikdy o nové články.",
"battery_fleet_status_low": "Nízká",
"battery_fleet_status_soon": "Brzy",
"battery_fleet_status_ok": "V pořádku",
@@ -6,6 +6,7 @@
"due_soon": "Snart forfalden",
"triggered": "Udløst",
"trigger_replaced": "Udløser erstattet",
"trigger_removed": "Udløser fjernet",
"ok": "OK",
"all": "Alle",
"new_object": "+ Nyt objekt",
@@ -518,6 +519,7 @@
"group_by_label": "Grupper efter",
"state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.",
"target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).",
"for_minutes_state_help": "0 tæller hver ændring straks. Med minutter skal den nye tilstand først holde — korte blink hverken udløser eller tælles så.",
"qr_print_title": "Udskriv QR-koder",
"qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.",
"qr_print_load": "Indlæs objekter",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Forudsat rettidig fuldførelse.",
"schedule_preview_ends": "(serien slutter)",
"adopt_problem_responsible": "Ansvarlig bruger for alle overtagne opgaver (valgfrit)",
"adopt_for_minutes_hint": "Udløs først når problemet har varet så længe — 0 reagerer på første blink.",
"adopt_problem_configure": "Konfigurer",
"history_auto": "Automatisk",
"battery_fleet_title": "Batteriflåde",
@@ -838,6 +841,8 @@
"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_add": "Tilføj et batteri",
"battery_fleet_add_hint": "Vælg en batterisensor, som den automatiske registrering overså — den kommer straks på listen.",
"battery_fleet_track_self": "Spor selvopladende batterier",
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvopladende enheder vises som genopladelige — ved lavt niveau bedes om opladning, aldrig om nye batterier.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
@@ -6,6 +6,7 @@
"due_soon": "Bald fällig",
"triggered": "Ausgelöst",
"trigger_replaced": "Auslöser ersetzt",
"trigger_removed": "Trigger entfernt",
"ok": "OK",
"all": "Alle",
"new_object": "+ Neues Objekt",
@@ -518,6 +519,7 @@
"group_by_label": "Gruppieren nach",
"state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z.B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.",
"target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).",
"for_minutes_state_help": "0 zählt jede Änderung sofort. Mit Minuten muss der neue Zustand erst so lange bestehen — kurzes Flackern löst dann weder aus noch zählt es.",
"qr_print_title": "QR-Codes drucken",
"qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.",
"qr_print_load": "Objekte laden",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Bei pünktlichem Abschluss.",
"schedule_preview_ends": "(Serie endet)",
"adopt_problem_responsible": "Verantwortliche Person für alle übernommenen Aufgaben (optional)",
"adopt_for_minutes_hint": "Erst auslösen, wenn das Problem so lange besteht — 0 reagiert auf das erste Flackern.",
"adopt_problem_configure": "Konfigurieren",
"history_auto": "Automatisch",
"battery_fleet_title": "Batterie-Flotte",
@@ -838,6 +841,8 @@
"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_add": "Batterie hinzufügen",
"battery_fleet_add_hint": "Einen Batteriesensor wählen, den die automatische Erkennung übersehen hat — er erscheint sofort in der Liste.",
"battery_fleet_track_self": "Selbstladende Batterien mitverfolgen",
"battery_fleet_track_self_hint": "Telefone, Staubsauger und andere selbstladende Geräte erscheinen als Akkus — bei niedrigem Stand wird ans Laden erinnert, nie an neue Zellen.",
"battery_fleet_status_low": "Schwach",
"battery_fleet_status_soon": "Bald",
"battery_fleet_status_ok": "In Ordnung",
@@ -6,6 +6,7 @@
"due_soon": "Due Soon",
"triggered": "Triggered",
"trigger_replaced": "Trigger replaced",
"trigger_removed": "Trigger removed",
"ok": "OK",
"all": "All",
"new_object": "+ New Object",
@@ -524,6 +525,7 @@
"group_by_label": "Group by",
"state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.",
"target_changes_help": "Number of matching transitions before the trigger fires (default: 1).",
"for_minutes_state_help": "0 counts every change immediately. Set minutes and the new state must hold that long first — brief flickers then neither trigger nor count.",
"qr_print_title": "Print QR codes",
"qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.",
"qr_print_load": "Load objects",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Assuming on-time completion.",
"schedule_preview_ends": "(series ends)",
"adopt_problem_responsible": "Responsible user for all adopted tasks (optional)",
"adopt_for_minutes_hint": "Only trigger once the problem has persisted this long — 0 reacts to the first flicker.",
"adopt_problem_configure": "Configure",
"history_auto": "Automatic",
"battery_fleet_title": "Battery fleet",
@@ -838,6 +841,8 @@
"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_add": "Add a battery",
"battery_fleet_add_hint": "Pick a battery sensor the automatic discovery missed — it joins the roster immediately.",
"battery_fleet_track_self": "Track self-charging batteries",
"battery_fleet_track_self_hint": "Phones, vacuums and other devices that recharge themselves appear as rechargeables — a low one asks for a charge, never for new cells.",
"battery_fleet_status_low": "Low",
"battery_fleet_status_soon": "Soon",
"battery_fleet_status_ok": "Healthy",
@@ -517,6 +517,7 @@
"group_by_label": "Agrupar por",
"state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.",
"target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).",
"for_minutes_state_help": "0 cuenta cada cambio de inmediato. Con minutos, el nuevo estado debe mantenerse primero — un parpadeo breve entonces ni dispara ni cuenta.",
"qr_print_title": "Imprimir códigos QR",
"qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.",
"qr_print_load": "Cargar objetos",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "No era necesario",
"quick_complete_success": "Completado rápido",
"trigger_replaced": "Disparador reemplazado",
"trigger_removed": "Disparador eliminado",
"add": "Añadir",
"show_stats": "Mostrar estadísticas + gráficos",
"hide_stats": "Ocultar estadísticas",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Suponiendo finalización puntual.",
"schedule_preview_ends": "(la serie termina)",
"adopt_problem_responsible": "Usuario responsable para todas las tareas adoptadas (opcional)",
"adopt_for_minutes_hint": "Disparar solo si el problema persiste este tiempo — 0 reacciona al primer parpadeo.",
"adopt_problem_configure": "Configurar",
"history_auto": "Automático",
"battery_fleet_title": "Flota de baterías",
@@ -838,6 +841,8 @@
"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_add": "Añadir una batería",
"battery_fleet_add_hint": "Elige un sensor de batería que la detección automática pasó por alto — se une a la lista de inmediato.",
"battery_fleet_track_self": "Seguir baterías de carga automática",
"battery_fleet_track_self_hint": "Teléfonos, aspiradoras y otros dispositivos que se recargan solos aparecen como recargables: si están bajos piden una carga, nunca pilas nuevas.",
"battery_fleet_status_low": "Baja",
"battery_fleet_status_soon": "Pronto",
"battery_fleet_status_ok": "Correcta",
@@ -6,6 +6,7 @@
"due_soon": "Pian erääntyy",
"triggered": "Laukaistu",
"trigger_replaced": "Laukaisin korvattu",
"trigger_removed": "Laukaisin poistettu",
"ok": "OK",
"all": "Kaikki",
"new_object": "+ Uusi kohde",
@@ -518,6 +519,7 @@
"group_by_label": "Ryhmittele",
"state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.",
"target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).",
"for_minutes_state_help": "0 laskee jokaisen muutoksen heti. Minuuteilla uuden tilan on ensin pysyttävä — lyhyet välähdykset eivät silloin laukaise eivätkä kirjaudu.",
"qr_print_title": "Tulosta QR-koodit",
"qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.",
"qr_print_load": "Lataa kohteet",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Olettaen, että valmistuminen on ajallaan.",
"schedule_preview_ends": "(sarja päättyy)",
"adopt_problem_responsible": "Vastuuhenkilö kaikille omaksutuille tehtäville (valinnainen)",
"adopt_for_minutes_hint": "Laukaise vasta kun ongelma on kestänyt näin kauan — 0 reagoi ensimmäiseen välähdykseen.",
"adopt_problem_configure": "Määritä",
"history_auto": "Automaattinen",
"battery_fleet_title": "Paristokanta",
@@ -838,6 +841,8 @@
"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_add": "Lisää paristo",
"battery_fleet_add_hint": "Valitse paristoanturi, jonka automaattinen tunnistus ohitti — se liittyy luetteloon heti.",
"battery_fleet_track_self": "Seuraa itselatautuvia akkuja",
"battery_fleet_track_self_hint": "Puhelimet, imurit ja muut itselatautuvat laitteet näkyvät ladattavina — vähissä oleva pyytää latausta, ei koskaan uusia paristoja.",
"battery_fleet_status_low": "Vähissä",
"battery_fleet_status_soon": "Pian",
"battery_fleet_status_ok": "Kunnossa",
@@ -517,6 +517,7 @@
"group_by_label": "Grouper par",
"state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p.ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.",
"target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut: 1).",
"for_minutes_state_help": "0 compte chaque changement immédiatement. Avec des minutes, le nouvel état doit d'abord se maintenir — un bref clignotement ne déclenche ni ne compte alors.",
"qr_print_title": "Imprimer les QR codes",
"qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.",
"qr_print_load": "Charger les objets",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Non nécessaire",
"quick_complete_success": "Terminé rapidement",
"trigger_replaced": "Déclencheur remplacé",
"trigger_removed": "Déclencheur supprimé",
"add": "Ajouter",
"show_stats": "Afficher les statistiques + graphiques",
"hide_stats": "Masquer les statistiques",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "En supposant un achèvement ponctuel.",
"schedule_preview_ends": "(fin de la série)",
"adopt_problem_responsible": "Personne responsable pour toutes les tâches adoptées (optionnel)",
"adopt_for_minutes_hint": "Ne déclencher que si le problème persiste aussi longtemps — 0 réagit au premier clignotement.",
"adopt_problem_configure": "Configurer",
"history_auto": "Automatique",
"battery_fleet_title": "Parc de piles",
@@ -838,6 +841,8 @@
"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_add": "Ajouter une batterie",
"battery_fleet_add_hint": "Choisissez un capteur de batterie que la détection automatique a manqué — il rejoint la liste immédiatement.",
"battery_fleet_track_self": "Suivre les batteries à recharge automatique",
"battery_fleet_track_self_hint": "Les téléphones, aspirateurs et autres appareils qui se rechargent seuls apparaissent comme rechargeables — un niveau bas demande une recharge, jamais de nouvelles piles.",
"battery_fleet_status_low": "Faible",
"battery_fleet_status_soon": "Bientôt",
"battery_fleet_status_ok": "Bon état",
@@ -6,6 +6,7 @@
"due_soon": "जल्द देय",
"triggered": "ट्रिगर हुआ",
"trigger_replaced": "ट्रिगर बदला गया",
"trigger_removed": "ट्रिगर हटाया गया",
"ok": "ठीक",
"all": "सभी",
"new_object": "+ नई वस्तु",
@@ -518,6 +519,7 @@
"group_by_label": "इसके अनुसार समूहित करें",
"state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।",
"target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।",
"for_minutes_state_help": "0 हर परिवर्तन को तुरंत गिनता है। मिनट सेट करने पर नई स्थिति को पहले उतनी देर बनी रहनी होगी — क्षणिक झिलमिलाहट तब न ट्रिगर करती है न गिनी जाती है।",
"qr_print_title": "QR कोड प्रिंट करें",
"qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।",
"qr_print_load": "वस्तुएँ लोड करें",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "समय पर पूर्णता मानते हुए।",
"schedule_preview_ends": "(श्रृंखला समाप्त)",
"adopt_problem_responsible": "सभी अपनाए गए कार्यों के लिए ज़िम्मेदार उपयोगकर्ता (वैकल्पिक)",
"adopt_for_minutes_hint": "समस्या इतनी देर बनी रहने पर ही ट्रिगर करें — 0 पहली झिलमिलाहट पर प्रतिक्रिया करता है।",
"adopt_problem_configure": "कॉन्फ़िगर करें",
"history_auto": "स्वचालित",
"battery_fleet_title": "बैटरी फ्लीट",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "किसी डिवाइस को यहाँ कम बैटरी बताने से पहले ही बाहर रखें — जैसे खुद चार्ज होने वाला वैक्यूम, या खुद ही चेतावनी देने वाला फ़ोन।",
"battery_fleet_add": "बैटरी जोड़ें",
"battery_fleet_add_hint": "स्वचालित खोज से छूटा बैटरी सेंसर चुनें — यह तुरंत सूची में जुड़ जाएगा।",
"battery_fleet_track_self": "स्वयं चार्ज होने वाली बैटरियों को ट्रैक करें",
"battery_fleet_track_self_hint": "फ़ोन, वैक्यूम और अन्य स्वयं चार्ज होने वाले उपकरण रिचार्जेबल के रूप में दिखते हैं — कम होने पर चार्ज करने को कहा जाता है, नई बैटरी कभी नहीं।",
"battery_fleet_status_low": "कम",
"battery_fleet_status_soon": "जल्द",
"battery_fleet_status_ok": "ठीक",
@@ -6,6 +6,7 @@
"due_soon": "Hamarosan esedékes",
"triggered": "Aktiválva",
"trigger_replaced": "Kiváltó lecserélve",
"trigger_removed": "Eseményindító eltávolítva",
"ok": "OK",
"all": "Összes",
"new_object": "+ Új objektum",
@@ -524,6 +525,7 @@
"group_by_label": "Csoportosítás",
"state_value_help": "A HA állapotértékét használja (általában kisbetűs, pl. „on”/„off”). A kis- és nagybetűk mentéskor normalizálódnak.",
"target_changes_help": "Az egyező átmenetek száma a kiváltás előtt (alapértelmezés: 1).",
"for_minutes_state_help": "A 0 minden változást azonnal számol. Percekkel az új állapotnak előbb fenn kell maradnia — a rövid villanás ekkor se nem indít, se nem számít.",
"qr_print_title": "QR-kódok nyomtatása",
"qr_print_desc": "Nyomtatható oldal QR-kódokkal — vágja ki és ragassza fel a berendezésekre.",
"qr_print_load": "Objektumok betöltése",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Időben történő elvégzést feltételezve.",
"schedule_preview_ends": "(a sorozat véget ér)",
"adopt_problem_responsible": "Felelős felhasználó az összes átvett feladathoz (opcionális)",
"adopt_for_minutes_hint": "Csak akkor induljon, ha a probléma ennyi ideig fennáll — a 0 az első villanásra reagál.",
"adopt_problem_configure": "Beállítás",
"history_auto": "Automatikus",
"battery_fleet_title": "Elemflotta",
@@ -838,6 +841,8 @@
"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_add": "Elem hozzáadása",
"battery_fleet_add_hint": "Válassz egy elem-érzékelőt, amelyet az automatikus felismerés kihagyott — azonnal felkerül a listára.",
"battery_fleet_track_self": "Öntöltő akkumulátorok követése",
"battery_fleet_track_self_hint": "A telefonok, porszívók és más öntöltő eszközök akkumulátorként jelennek meg — alacsony töltöttségnél töltést kérnek, sosem új elemet.",
"battery_fleet_status_low": "Alacsony",
"battery_fleet_status_soon": "Hamarosan",
"battery_fleet_status_ok": "Rendben",
@@ -517,6 +517,7 @@
"group_by_label": "Raggruppa per",
"state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.",
"target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).",
"for_minutes_state_help": "0 conta ogni cambiamento subito. Con i minuti, il nuovo stato deve prima mantenersi — un breve sfarfallio allora non attiva né conta.",
"qr_print_title": "Stampa codici QR",
"qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.",
"qr_print_load": "Carica oggetti",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Non necessario",
"quick_complete_success": "Completato rapidamente",
"trigger_replaced": "Trigger sostituito",
"trigger_removed": "Trigger rimosso",
"add": "Aggiungi",
"show_stats": "Mostra statistiche + grafici",
"hide_stats": "Nascondi statistiche",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Presumendo un completamento puntuale.",
"schedule_preview_ends": "(la serie termina)",
"adopt_problem_responsible": "Utente responsabile per tutte le attività adottate (opzionale)",
"adopt_for_minutes_hint": "Attiva solo se il problema persiste per questo tempo — 0 reagisce al primo sfarfallio.",
"adopt_problem_configure": "Configura",
"history_auto": "Automatico",
"battery_fleet_title": "Parco batterie",
@@ -838,6 +841,8 @@
"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_add": "Aggiungi una batteria",
"battery_fleet_add_hint": "Scegli un sensore batteria sfuggito al rilevamento automatico — entra subito nell'elenco.",
"battery_fleet_track_self": "Traccia le batterie autoricaricabili",
"battery_fleet_track_self_hint": "Telefoni, aspirapolvere e altri dispositivi che si ricaricano da soli appaiono come ricaricabili — se scarichi chiedono una ricarica, mai batterie nuove.",
"battery_fleet_status_low": "Scarica",
"battery_fleet_status_soon": "A breve",
"battery_fleet_status_ok": "A posto",
@@ -6,6 +6,7 @@
"due_soon": "期限間近",
"triggered": "トリガー発生",
"trigger_replaced": "トリガー解除済み",
"trigger_removed": "トリガーを削除しました",
"ok": "OK",
"all": "すべて",
"new_object": "+ 新規対象",
@@ -518,6 +519,7 @@
"group_by_label": "グループ化",
"state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。",
"target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。",
"for_minutes_state_help": "0 はすべての変化を即座にカウントします。分を設定すると新しい状態がその時間続く必要があり、一瞬の変動はトリガーもカウントもされません。",
"qr_print_title": "QRコードを印刷",
"qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。",
"qr_print_load": "対象を読み込む",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "期日どおりに完了した場合。",
"schedule_preview_ends": "(シリーズ終了)",
"adopt_problem_responsible": "採用したすべてのタスクの担当ユーザー(任意)",
"adopt_for_minutes_hint": "問題がこの時間続いた場合にのみトリガーします。0 は最初の変動に即反応します。",
"adopt_problem_configure": "設定",
"history_auto": "自動",
"battery_fleet_title": "電池フリート",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "電池残量が少ないと報告される前に、ここで機器を除外できます — 自動充電するロボット掃除機や、自分で警告するスマートフォンなど。",
"battery_fleet_add": "電池を追加",
"battery_fleet_add_hint": "自動検出が見逃した電池センサーを選択すると、すぐに一覧に追加されます。",
"battery_fleet_track_self": "自己充電式バッテリーを追跡",
"battery_fleet_track_self_hint": "スマートフォンや掃除機など自己充電するデバイスは充電式として表示されます。残量が少ないときは充電を促し、電池交換は求めません。",
"battery_fleet_status_low": "残量低下",
"battery_fleet_status_soon": "まもなく",
"battery_fleet_status_ok": "良好",
@@ -6,6 +6,7 @@
"due_soon": "기한 임박",
"triggered": "트리거됨",
"trigger_replaced": "트리거 교체됨",
"trigger_removed": "트리거 제거됨",
"ok": "정상",
"all": "전체",
"new_object": "+ 새 객체",
@@ -524,6 +525,7 @@
"group_by_label": "그룹화",
"state_value_help": "HA 상태 값을 사용하세요(보통 소문자, 예: \"on\"/\"off\"). 대소문자는 저장 시 정규화됩니다.",
"target_changes_help": "트리거가 발동하기까지 필요한 일치 전환 횟수입니다 (기본값: 1).",
"for_minutes_state_help": "0은 모든 변화를 즉시 집계합니다. 분을 설정하면 새 상태가 먼저 그 시간 동안 유지되어야 하며, 짧은 깜빡임은 트리거되지도 집계되지도 않습니다.",
"qr_print_title": "QR 코드 인쇄",
"qr_print_desc": "잘라서 장비에 붙일 수 있는 QR 코드 인쇄 페이지를 생성합니다.",
"qr_print_load": "객체 불러오기",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "제때 완료한다고 가정한 일정입니다.",
"schedule_preview_ends": "(시리즈 종료)",
"adopt_problem_responsible": "가져온 모든 작업의 담당자 (선택)",
"adopt_for_minutes_hint": "문제가 이 시간 동안 지속된 경우에만 트리거합니다. 0은 첫 깜빡임에 즉시 반응합니다.",
"adopt_problem_configure": "구성",
"history_auto": "자동",
"battery_fleet_title": "배터리 플릿",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "배터리 부족을 알리기 전에 여기에서 기기를 제외하세요 — 스스로 충전하는 로봇청소기나 자체적으로 경고하는 휴대폰처럼.",
"battery_fleet_add": "배터리 추가",
"battery_fleet_add_hint": "자동 검색이 놓친 배터리 센서를 선택하면 즉시 목록에 추가됩니다.",
"battery_fleet_track_self": "자체 충전 배터리 추적",
"battery_fleet_track_self_hint": "휴대폰, 청소기 등 스스로 충전되는 기기는 충전식으로 표시됩니다. 잔량이 낮으면 충전을 요청하며 새 전지를 요구하지 않습니다.",
"battery_fleet_status_low": "부족",
"battery_fleet_status_soon": "곧",
"battery_fleet_status_ok": "정상",
@@ -6,6 +6,7 @@
"due_soon": "Forfaller snart",
"triggered": "Utløst",
"trigger_replaced": "Utløser erstattet",
"trigger_removed": "Utløser fjernet",
"ok": "OK",
"all": "Alle",
"new_object": "+ Nytt objekt",
@@ -518,6 +519,7 @@
"group_by_label": "Grupper etter",
"state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.",
"target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).",
"for_minutes_state_help": "0 teller hver endring umiddelbart. Med minutter må den nye tilstanden først holde seg — korte blink verken utløser eller telles da.",
"qr_print_title": "Skriv ut QR-koder",
"qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.",
"qr_print_load": "Last objekter",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Forutsatt fullføring i tide.",
"schedule_preview_ends": "(serien avsluttes)",
"adopt_problem_responsible": "Ansvarlig bruker for alle overtatte oppgaver (valgfritt)",
"adopt_for_minutes_hint": "Utløs først når problemet har vart så lenge — 0 reagerer på første blink.",
"adopt_problem_configure": "Konfigurer",
"history_auto": "Automatisk",
"battery_fleet_title": "Batteriflåte",
@@ -838,6 +841,8 @@
"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_add": "Legg til et batteri",
"battery_fleet_add_hint": "Velg en batterisensor som den automatiske gjenkjenningen overså — den blir med i listen umiddelbart.",
"battery_fleet_track_self": "Spor selvladende batterier",
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvladende enheter vises som oppladbare — ved lavt nivå bes det om lading, aldri om nye batterier.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
@@ -517,6 +517,7 @@
"group_by_label": "Groeperen op",
"state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.",
"target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).",
"for_minutes_state_help": "0 telt elke wijziging direct. Met minuten moet de nieuwe status eerst zo lang aanhouden — kort flikkeren triggert dan niet en telt niet mee.",
"qr_print_title": "QR-codes afdrukken",
"qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.",
"qr_print_load": "Objecten laden",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Niet nodig",
"quick_complete_success": "Snel als voltooid gemarkeerd",
"trigger_replaced": "Trigger vervangen",
"trigger_removed": "Trigger verwijderd",
"add": "Toevoegen",
"show_stats": "Statistieken + grafieken tonen",
"hide_stats": "Statistieken verbergen",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Uitgaande van tijdige afronding.",
"schedule_preview_ends": "(reeks eindigt)",
"adopt_problem_responsible": "Verantwoordelijke gebruiker voor alle overgenomen taken (optioneel)",
"adopt_for_minutes_hint": "Pas triggeren als het probleem zo lang aanhoudt — 0 reageert op de eerste flikkering.",
"adopt_problem_configure": "Configureren",
"history_auto": "Automatisch",
"battery_fleet_title": "Batterijvloot",
@@ -838,6 +841,8 @@
"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_add": "Batterij toevoegen",
"battery_fleet_add_hint": "Kies een batterijsensor die de automatische detectie heeft gemist — hij verschijnt direct in de lijst.",
"battery_fleet_track_self": "Zelfopladende batterijen volgen",
"battery_fleet_track_self_hint": "Telefoons, stofzuigers en andere zelfopladende apparaten verschijnen als oplaadbaar — bij een lage stand wordt om opladen gevraagd, nooit om nieuwe batterijen.",
"battery_fleet_status_low": "Laag",
"battery_fleet_status_soon": "Binnenkort",
"battery_fleet_status_ok": "In orde",
@@ -517,6 +517,7 @@
"group_by_label": "Grupuj wg",
"state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.",
"target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).",
"for_minutes_state_help": "0 liczy każdą zmianę od razu. Z minutami nowy stan musi się najpierw utrzymać — krótkie mignięcia wtedy nie wyzwalają i nie liczą się.",
"qr_print_title": "Drukuj kody QR",
"qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.",
"qr_print_load": "Załaduj obiekty",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Nie było potrzebne",
"quick_complete_success": "Szybko oznaczono jako wykonane",
"trigger_replaced": "Wyzwalacz zastąpiony",
"trigger_removed": "Wyzwalacz usunięty",
"add": "Dodaj",
"show_stats": "Pokaż statystyki + wykresy",
"hide_stats": "Ukryj statystyki",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Przy założeniu terminowego ukończenia.",
"schedule_preview_ends": "(seria się kończy)",
"adopt_problem_responsible": "Osoba odpowiedzialna za wszystkie przejęte zadania (opcjonalnie)",
"adopt_for_minutes_hint": "Wyzwalaj dopiero, gdy problem utrzymuje się tak długo — 0 reaguje na pierwsze mignięcie.",
"adopt_problem_configure": "Konfiguruj",
"history_auto": "Automatycznie",
"battery_fleet_title": "Flota baterii",
@@ -838,6 +841,8 @@
"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_add": "Dodaj baterię",
"battery_fleet_add_hint": "Wybierz czujnik baterii pominięty przez automatyczne wykrywanie — od razu trafi na listę.",
"battery_fleet_track_self": "Śledź baterie samoładujące",
"battery_fleet_track_self_hint": "Telefony, odkurzacze i inne samoładujące się urządzenia pojawiają się jako akumulatory — przy niskim poziomie proszą o ładowanie, nigdy o nowe baterie.",
"battery_fleet_status_low": "Niski",
"battery_fleet_status_soon": "Wkrótce",
"battery_fleet_status_ok": "W porządku",
@@ -6,6 +6,7 @@
"due_soon": "Vence em breve",
"triggered": "Acionada",
"trigger_replaced": "Gatilho substituído",
"trigger_removed": "Gatilho removido",
"ok": "OK",
"all": "Todas",
"new_object": "+ Novo objeto",
@@ -524,6 +525,7 @@
"group_by_label": "Agrupar por",
"state_value_help": "Use o valor de estado do HA (geralmente minúsculo, ex.: \"on\"/\"off\"). Maiúsculas e minúsculas são normalizadas ao salvar.",
"target_changes_help": "Número de transições correspondentes antes de o gatilho disparar (padrão: 1).",
"for_minutes_state_help": "0 conta cada mudança imediatamente. Com minutos, o novo estado deve se manter primeiro — uma oscilação breve então não dispara nem conta.",
"qr_print_title": "Imprimir códigos QR",
"qr_print_desc": "Gere uma página imprimível de códigos QR para recortar e colar nos seus equipamentos.",
"qr_print_load": "Carregar objetos",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Supondo conclusão em dia.",
"schedule_preview_ends": "(a série termina)",
"adopt_problem_responsible": "Usuário responsável por todas as tarefas adotadas (opcional)",
"adopt_for_minutes_hint": "Disparar apenas se o problema persistir por esse tempo — 0 reage à primeira oscilação.",
"adopt_problem_configure": "Configurar",
"history_auto": "Automática",
"battery_fleet_title": "Frota de baterias",
@@ -838,6 +841,8 @@
"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_add": "Adicionar uma bateria",
"battery_fleet_add_hint": "Escolha um sensor de bateria que a detecção automática não encontrou — ele entra na lista imediatamente.",
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem uma recarga, nunca pilhas novas.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
@@ -513,6 +513,7 @@
"group_by_label": "Agrupar por",
"state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.",
"target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).",
"for_minutes_state_help": "0 conta cada mudança de imediato. Com minutos, o novo estado tem de se manter primeiro — uma oscilação breve então não dispara nem conta.",
"qr_print_title": "Imprimir códigos QR",
"qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.",
"qr_print_load": "Carregar objetos",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Não era necessário",
"quick_complete_success": "Concluído rapidamente",
"trigger_replaced": "Acionador substituído",
"trigger_removed": "Gatilho removido",
"add": "Adicionar",
"show_stats": "Mostrar estatísticas + gráficos",
"hide_stats": "Ocultar estatísticas",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Assumindo conclusão pontual.",
"schedule_preview_ends": "(a série termina)",
"adopt_problem_responsible": "Utilizador responsável por todas as tarefas adotadas (opcional)",
"adopt_for_minutes_hint": "Disparar apenas se o problema persistir este tempo — 0 reage à primeira oscilação.",
"adopt_problem_configure": "Configurar",
"history_auto": "Automático",
"battery_fleet_title": "Frota de baterias",
@@ -838,6 +841,8 @@
"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_add": "Adicionar uma bateria",
"battery_fleet_add_hint": "Escolha um sensor de bateria que a deteção automática não encontrou — entra de imediato na lista.",
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem um carregamento, nunca pilhas novas.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
@@ -517,6 +517,7 @@
"group_by_label": "Группировать по",
"state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.",
"target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).",
"for_minutes_state_help": "0 — каждое изменение засчитывается сразу. С минутами новое состояние сначала должно удержаться — короткие сбои тогда не срабатывают и не считаются.",
"qr_print_title": "Печать QR-кодов",
"qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.",
"qr_print_load": "Загрузить объекты",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Не было необходимо",
"quick_complete_success": "Быстро отмечено как выполнено",
"trigger_replaced": "Триггер заменён",
"trigger_removed": "Триггер удалён",
"add": "Добавить",
"show_stats": "Показать статистику + графики",
"hide_stats": "Скрыть статистику",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "При условии своевременного выполнения.",
"schedule_preview_ends": "(серия завершается)",
"adopt_problem_responsible": "Ответственный пользователь для всех принятых задач (необязательно)",
"adopt_for_minutes_hint": "Срабатывать только если проблема сохраняется это время — 0 реагирует на первый сбой.",
"adopt_problem_configure": "Настроить",
"history_auto": "Автоматически",
"battery_fleet_title": "Парк батарей",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "Исключите устройство здесь ещё до того, как оно сообщит о низком заряде — робот-пылесос, который заряжается сам, или телефон, который предупреждает вас сам.",
"battery_fleet_add": "Добавить батарею",
"battery_fleet_add_hint": "Выберите датчик батареи, который автоматическое обнаружение пропустило — он сразу появится в списке.",
"battery_fleet_track_self": "Отслеживать самозаряжающиеся батареи",
"battery_fleet_track_self_hint": "Телефоны, пылесосы и другие самозаряжающиеся устройства отображаются как аккумуляторы — при низком заряде нужна зарядка, а не новые батарейки.",
"battery_fleet_status_low": "Низкий",
"battery_fleet_status_soon": "Скоро",
"battery_fleet_status_ok": "В норме",
@@ -517,6 +517,7 @@
"group_by_label": "Gruppera efter",
"state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.",
"target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).",
"for_minutes_state_help": "0 räknar varje ändring direkt. Med minuter måste det nya tillståndet först hålla i sig — korta blinkningar varken utlöser eller räknas då.",
"qr_print_title": "Skriv ut QR-koder",
"qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.",
"qr_print_load": "Ladda objekt",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Inte nödvändigt",
"quick_complete_success": "Snabbt markerad som klar",
"trigger_replaced": "Utlösare ersatt",
"trigger_removed": "Utlösare borttagen",
"add": "Lägg till",
"show_stats": "Visa statistik + grafer",
"hide_stats": "Dölj statistik",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Förutsatt slutförande i tid.",
"schedule_preview_ends": "(serien avslutas)",
"adopt_problem_responsible": "Ansvarig användare för alla övertagna uppgifter (valfritt)",
"adopt_for_minutes_hint": "Utlös först när problemet kvarstått så här länge — 0 reagerar på första blinkningen.",
"adopt_problem_configure": "Konfigurera",
"history_auto": "Automatiskt",
"battery_fleet_title": "Batteriflotta",
@@ -838,6 +841,8 @@
"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_add": "Lägg till ett batteri",
"battery_fleet_add_hint": "Välj en batterisensor som den automatiska identifieringen missade — den läggs till i listan direkt.",
"battery_fleet_track_self": "Spåra självladdande batterier",
"battery_fleet_track_self_hint": "Telefoner, dammsugare och andra självladdande enheter visas som laddningsbara — vid låg nivå begärs laddning, aldrig nya batterier.",
"battery_fleet_status_low": "Låg",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I ordning",
@@ -6,6 +6,7 @@
"due_soon": "Yaklaşan",
"triggered": "Tetiklendi",
"trigger_replaced": "Tetikleyici değiştirildi",
"trigger_removed": "Tetikleyici kaldırıldı",
"ok": "Tamam",
"all": "Tümü",
"new_object": "+ Yeni Nesne",
@@ -524,6 +525,7 @@
"group_by_label": "Grupla",
"state_value_help": "HA durum değerini kullanın (genellikle küçük harf, örn. \"on\"/\"off\"). Büyük/küçük harf kaydederken normalleştirilir.",
"target_changes_help": "Tetikleyici çalışmadan önceki eşleşen geçiş sayısı (varsayılan: 1).",
"for_minutes_state_help": "0 her değişikliği hemen sayar. Dakika ayarlanırsa yeni durumun önce o kadar sürmesi gerekir — kısa titremeler o zaman ne tetikler ne de sayılır.",
"qr_print_title": "QR kodlarını yazdır",
"qr_print_desc": "Kesip ekipmanınıza yapıştırmak için yazdırılabilir bir QR kodu sayfası oluşturun.",
"qr_print_load": "Nesneleri yükle",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Zamanında tamamlama varsayılarak.",
"schedule_preview_ends": "(seri sona erer)",
"adopt_problem_responsible": "Devralınan tüm görevler için sorumlu kullanıcı (isteğe bağlı)",
"adopt_for_minutes_hint": "Yalnızca sorun bu süre boyunca devam ederse tetikle — 0 ilk titremeye tepki verir.",
"adopt_problem_configure": "Yapılandır",
"history_auto": "Otomatik",
"battery_fleet_title": "Pil filosu",
@@ -838,6 +841,8 @@
"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_add": "Pil ekle",
"battery_fleet_add_hint": "Otomatik keşfin atladığı bir pil sensörü seçin — hemen listeye katılır.",
"battery_fleet_track_self": "Kendi kendine şarj olan pilleri izle",
"battery_fleet_track_self_hint": "Telefonlar, süpürgeler ve kendi kendine şarj olan diğer cihazlar şarj edilebilir olarak görünür — seviye düşükken şarj istenir, asla yeni pil istenmez.",
"battery_fleet_status_low": "Düşük",
"battery_fleet_status_soon": "Yakında",
"battery_fleet_status_ok": "İyi durumda",
@@ -517,6 +517,7 @@
"group_by_label": "Групувати за",
"state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.",
"target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).",
"for_minutes_state_help": "0 — кожна зміна зараховується одразу. З хвилинами новий стан спершу має утриматися — короткі збої тоді не спрацьовують і не рахуються.",
"qr_print_title": "Друкувати QR-коди",
"qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.",
"qr_print_load": "Завантажити об'єкти",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Не було необхідно",
"quick_complete_success": "Швидко позначено виконаним",
"trigger_replaced": "Тригер замінено",
"trigger_removed": "Тригер видалено",
"add": "Додати",
"show_stats": "Показати статистику + графіки",
"hide_stats": "Сховати статистику",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "За умови вчасного виконання.",
"schedule_preview_ends": "(серія завершується)",
"adopt_problem_responsible": "Відповідальний користувач для всіх прийнятих завдань (необов'язково)",
"adopt_for_minutes_hint": "Спрацьовувати лише якщо проблема триває цей час — 0 реагує на перший збій.",
"adopt_problem_configure": "Налаштувати",
"history_auto": "Автоматично",
"battery_fleet_title": "Парк батарей",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "Вилучіть пристрій тут ще до того, як він повідомить про низький заряд — робот-пилосос, що заряджається сам, або телефон, який попереджає сам.",
"battery_fleet_add": "Додати батарею",
"battery_fleet_add_hint": "Виберіть датчик батареї, який автоматичне виявлення пропустило — він одразу з'явиться у списку.",
"battery_fleet_track_self": "Відстежувати самозарядні батареї",
"battery_fleet_track_self_hint": "Телефони, пилососи та інші самозарядні пристрої відображаються як акумулятори — за низького заряду потрібна зарядка, а не нові батарейки.",
"battery_fleet_status_low": "Низький",
"battery_fleet_status_soon": "Незабаром",
"battery_fleet_status_ok": "У нормі",
@@ -6,6 +6,7 @@
"due_soon": "即将到期",
"triggered": "已触发",
"trigger_replaced": "触发器已更换",
"trigger_removed": "触发器已移除",
"ok": "正常",
"all": "全部",
"new_object": "+ 新建维护项",
@@ -518,6 +519,7 @@
"group_by_label": "分组依据",
"state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。",
"target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。",
"for_minutes_state_help": "0 表示每次变化立即计数。设置分钟后,新状态需先保持该时长——短暂闪变既不触发也不计数。",
"qr_print_title": "打印二维码",
"qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。",
"qr_print_load": "加载设备",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "假设按时完成。",
"schedule_preview_ends": "(系列结束)",
"adopt_problem_responsible": "所有采用任务的负责用户(可选)",
"adopt_for_minutes_hint": "问题持续此时长后才触发——0 表示对首次闪变立即响应。",
"adopt_problem_configure": "配置",
"history_auto": "自动",
"battery_fleet_title": "电池群",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "在设备报告电量不足之前就可以在这里将其排除——例如会自行充电的扫地机器人,或本身就会提醒你的手机。",
"battery_fleet_add": "添加电池",
"battery_fleet_add_hint": "选择自动发现遗漏的电池传感器——它会立即加入列表。",
"battery_fleet_track_self": "跟踪自充电电池",
"battery_fleet_track_self_hint": "手机、吸尘器等自充电设备显示为可充电电池——电量低时提示充电,而不是更换电池。",
"battery_fleet_status_low": "电量低",
"battery_fleet_status_soon": "即将",
"battery_fleet_status_ok": "正常",
@@ -35,7 +35,7 @@ import {
type CalendarEvent,
} from "./helpers/calendar-bucket";
import { calendarStyles } from "./calendar-styles";
import { sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
import { syncLocaleFromHass, sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
import { registerCustomCard } from "./helpers/register-card";
import { openHistoryEditDialog, openTaskQuickActions } from "./dialog-mount";
import type {
@@ -139,12 +139,7 @@ export class MaintenanceCalendarCard extends LitElement {
updated(changedProps: Map<string, unknown>): void {
super.updated(changedProps);
// Dates/times follow the HA profile format, not just the language (#97).
if (changedProps.has("hass")) setDateTimePrefs(this.hass?.locale);
const lang = this.hass?.language;
if (lang && !isLocaleLoaded(lang)) {
ensureLocale(lang).then(() => this.requestUpdate());
}
syncLocaleFromHass(this, changedProps);
if (changedProps.has("hass") && this.hass) {
if (!this._dataLoaded) {
this._dataLoaded = true;
@@ -4,8 +4,9 @@ import { LitElement, html, css, nothing } from "lit";
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
import { hydrateObjects } from "./helpers/hydrate-objects";
import { property, state } from "lit/decorators.js";
import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
import { openSignedDocument } from "./helpers/document-url";
import { isSafeHttpUrl } from "./helpers/url";
import { registerCustomCard } from "./helpers/register-card";
import type {
HomeAssistant,
@@ -20,6 +21,9 @@ import { UserService } from "./user-service";
import { partsForCompletion } from "./helpers/shared-parts";
import "./maintenance-card-editor";
import "./components/complete-dialog";
// The Battery Fleet card ships in this globally-loaded bundle so it is
// available on every dashboard without another extra_module_url entry.
import "./components/battery-fleet-card";
import {
openCreateObjectDialog,
openCreateTaskDialog,
@@ -110,12 +114,7 @@ export class MaintenanceSupporterCard extends LitElement {
updated(changedProps: Map<string, unknown>): void {
super.updated(changedProps);
// Dates/times follow the HA profile format, not just the language (#97).
if (changedProps.has("hass")) setDateTimePrefs(this.hass?.locale);
const lang = this.hass?.language;
if (lang && !isLocaleLoaded(lang)) {
ensureLocale(lang).then(() => this.requestUpdate());
}
syncLocaleFromHass(this, changedProps);
// Assignee names: resolved once, only when the config allows it AND some
// task actually carries a responsible user. The condition is checked
// synchronously here — an unconditional call whose promise triggered a
@@ -239,7 +238,10 @@ export class MaintenanceSupporterCard extends LitElement {
* (the same route the panel uses, so it works in the Companion app). */
private async _openDoc(doc: CardDoc): Promise<void> {
if (doc.kind === "weblink" && doc.url) {
window.open(doc.url, "_blank", "noopener");
// Same guarded sink as every other weblink opener: never hand a
// non-http(s) URL to window.open (defense-in-depth against a link
// stored before the server-side scheme check existed).
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
return;
}
try {
@@ -5,9 +5,9 @@ import { isSafeHttpUrl } from "./helpers/url";
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
import { isStaleBundle } from "./helpers/bundle-version";
import { customElement, property, state } from "lit/decorators.js";
import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs, langOf } from "./styles";
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs, langOf } from "./styles";
import { LS_KEYS, lsGet, lsSet } from "./helpers/storage-keys";
import { openSignedDocument, signApiPath } from "./helpers/document-url";
import { openHtmlInNewTab, openSignedDocument, signApiPath } from "./helpers/document-url";
import { readObjectsCache, writeObjectsCache } from "./helpers/objects-cache";
import { hydrateObjects } from "./helpers/hydrate-objects";
import { daysProgress } from "./helpers/interval";
@@ -373,15 +373,7 @@ export class MaintenanceSupporterPanel extends LitElement {
updated(changedProps: Map<string, unknown>): void {
super.updated(changedProps);
// Dates/times follow the HA profile format, not just the language (#97).
if (changedProps.has("hass")) setDateTimePrefs(this.hass?.locale);
// Lazy-load the user's UI language (non-EN tables aren't bundled) and
// re-render once it arrives. EN is bundled, so strings read in English
// until then rather than as raw keys.
const lang = this.hass?.language;
if (lang && !isLocaleLoaded(lang)) {
ensureLocale(lang).then(() => this.requestUpdate());
}
syncLocaleFromHass(this, changedProps);
if (changedProps.has("hass") && this.hass) {
if (!this._dataLoaded) {
this._dataLoaded = true;
@@ -1477,9 +1469,7 @@ export class MaintenanceSupporterPanel extends LitElement {
this._currencySymbol,
new Date().toISOString(),
);
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
window.open(url, "_blank");
setTimeout(() => URL.revokeObjectURL(url), 60000);
openHtmlInNewTab(html);
}
private async _duplicateObject(entryId: string): Promise<void> {
@@ -1843,9 +1833,7 @@ export class MaintenanceSupporterPanel extends LitElement {
new Date().toISOString(),
partsLines,
);
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
window.open(url, "_blank");
setTimeout(() => URL.revokeObjectURL(url), 60000);
openHtmlInNewTab(html);
} finally {
this._actionLoading = false;
}
@@ -29,7 +29,11 @@ export interface HistoryContext {
readingDelta?: (entry: HistoryEntry) => number | null;
}
const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered"] as const;
// Every HistoryEntryType is filterable (drift audit 2026-08: the repair-flow
// types were missing, so their entries could not be isolated). Chips only
// render for types present in the task's history, so the two rare ones cost
// nothing on normal tasks.
const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered", "trigger_replaced", "trigger_removed"] as const;
export function renderHistoryFilters(task: MaintenanceTask, ctx: HistoryContext) {
const L = ctx.lang;
@@ -2,6 +2,7 @@
import { css } from "lit";
import EN from "./locales/en.json";
import { BUNDLE_VERSION } from "./helpers/bundle-version";
// Display fallback when the backend hasn't sent a currency symbol. The backend
// derives the real symbol from const.BUDGET_CURRENCIES[DEFAULT_BUDGET_CURRENCY].
@@ -47,8 +48,21 @@ const _localeGlobals: LocaleGlobals = (() => {
})();
const STORE = _localeGlobals.store;
// Each copy guarantees the bundled English fallback is present.
if (!STORE.en) STORE.en = EN as Translations;
/** Seed/extend the shared English table with this bundle's copy.
*
* MERGE, not first-wins (#135 regression): after an update, a cached app
* shell can still list an OLD sibling bundle (the card) next to the fresh
* panel. First-wins let the stale bundle's EN missing every new key
* claim the store, and the fresh panel rendered raw keys
* ("BATTERY_FLEET_ADD"). Existing entries win conflicts (steady state is
* unchanged); every bundle contributes the keys it knows, so a newer
* bundle's keys can never be shadowed by an older first-loader.
*/
export function seedEnglish(en: Record<string, string>): void {
STORE.en = Object.assign({}, en, STORE.en ?? {});
}
seedEnglish(EN as Translations);
/** Languages available as runtime-loaded JSON. Keep in sync with locales/. */
const SUPPORTED_LANGS = new Set<string>([
@@ -79,6 +93,22 @@ export function t(key: string, lang?: string): string {
return STORE[l]?.[key] ?? STORE.en[key] ?? key;
}
/** The per-`updated()` locale boot every top-level surface needs: date/time
* prefs follow the HA profile (#97), and the user's UI language is lazily
* fetched with a re-render once it arrives (EN is bundled, so strings read
* in English until then rather than as raw keys). Was copy-pasted across the
* panel and both cards (drift audit 2026-08). */
export function syncLocaleFromHass(
host: { hass?: { language?: string; locale?: unknown }; requestUpdate: () => void },
changedProps: Map<string, unknown>,
): void {
if (changedProps.has("hass")) setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0]);
const lang = host.hass?.language;
if (lang && !isLocaleLoaded(lang)) {
ensureLocale(lang).then(() => host.requestUpdate());
}
}
/**
* The UI language for a hass object the ONE rule every component's `_lang`
* getter delegates to. Before this helper the 23 hand-copied getters had
@@ -108,7 +138,11 @@ export function ensureLocale(lang?: string): Promise<void> {
return Promise.resolve();
}
if (!(l in _localeInflight)) {
_localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json`)
// Version-busted (#135, same family as #124): the locale files are served
// without Cache-Control, so browsers cache them heuristically — after an
// update a stale table would miss every new key and silently fall back to
// English for them. "dev" builds keep a stable URL.
_localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json?v=${BUNDLE_VERSION}`)
.then((r) => (r.ok ? r.json() : null))
.then((data) => {
if (data) {
@@ -1067,48 +1101,9 @@ export const sharedStyles = css`
.confidence-dot.medium { background: var(--warning-color, #ff9800); }
.confidence-dot.high { background: var(--success-color, #4caf50); }
/* Feedback toggle buttons in complete dialog */
.feedback-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
border-top: 1px solid var(--divider-color);
}
.feedback-label {
font-weight: 500;
font-size: 13px;
color: var(--secondary-text-color);
}
.feedback-buttons {
display: flex;
gap: 8px;
}
.feedback-btn {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--divider-color);
border-radius: 8px;
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
font-size: 13px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.feedback-btn:hover {
background: var(--secondary-background-color, #f5f5f5);
}
.feedback-btn.selected {
background: var(--primary-color);
color: var(--text-primary-color, #fff);
border-color: var(--primary-color);
}
/* (The complete-dialog's .feedback-* rules live in complete-dialog.ts
this sheet carried a dead byte-identical copy until the 2026-08 drift
audit; complete-dialog never imports sharedStyles.) */
/* Seasonal chart */
.seasonal-chart {
@@ -517,6 +517,7 @@
"group_by_label": "Seskupit podle",
"state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.",
"target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).",
"for_minutes_state_help": "0 počítá každou změnu ihned. S minutami musí nový stav nejprve vydržet — krátké bliknutí pak nespustí ani se nepočítá.",
"qr_print_title": "Tisk QR kódů",
"qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.",
"qr_print_load": "Načíst objekty",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Nebylo potřeba",
"quick_complete_success": "Rychle označeno jako hotové",
"trigger_replaced": "Spouštěč nahrazen",
"trigger_removed": "Spouštěč odebrán",
"add": "Přidat",
"show_stats": "Zobrazit statistiky + grafy",
"hide_stats": "Skrýt statistiky",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Za předpokladu včasného dokončení.",
"schedule_preview_ends": "(série končí)",
"adopt_problem_responsible": "Odpovědná osoba pro všechny převzaté úkoly (volitelné)",
"adopt_for_minutes_hint": "Spustit až když problém trvá takto dlouho — 0 reaguje na první bliknutí.",
"adopt_problem_configure": "Konfigurovat",
"history_auto": "Automaticky",
"battery_fleet_title": "Flotila baterií",
@@ -838,6 +841,8 @@
"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_add": "Přidat baterii",
"battery_fleet_add_hint": "Vyberte senzor baterie, který automatická detekce přehlédla — okamžitě se objeví v seznamu.",
"battery_fleet_track_self": "Sledovat samodobíjecí baterie",
"battery_fleet_track_self_hint": "Telefony, vysavače a další samodobíjecí zařízení se zobrazují jako akumulátory — při nízkém stavu žádají o nabití, nikdy o nové články.",
"battery_fleet_status_low": "Nízká",
"battery_fleet_status_soon": "Brzy",
"battery_fleet_status_ok": "V pořádku",
@@ -6,6 +6,7 @@
"due_soon": "Snart forfalden",
"triggered": "Udløst",
"trigger_replaced": "Udløser erstattet",
"trigger_removed": "Udløser fjernet",
"ok": "OK",
"all": "Alle",
"new_object": "+ Nyt objekt",
@@ -518,6 +519,7 @@
"group_by_label": "Grupper efter",
"state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.",
"target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).",
"for_minutes_state_help": "0 tæller hver ændring straks. Med minutter skal den nye tilstand først holde — korte blink hverken udløser eller tælles så.",
"qr_print_title": "Udskriv QR-koder",
"qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.",
"qr_print_load": "Indlæs objekter",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Forudsat rettidig fuldførelse.",
"schedule_preview_ends": "(serien slutter)",
"adopt_problem_responsible": "Ansvarlig bruger for alle overtagne opgaver (valgfrit)",
"adopt_for_minutes_hint": "Udløs først når problemet har varet så længe — 0 reagerer på første blink.",
"adopt_problem_configure": "Konfigurer",
"history_auto": "Automatisk",
"battery_fleet_title": "Batteriflåde",
@@ -838,6 +841,8 @@
"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_add": "Tilføj et batteri",
"battery_fleet_add_hint": "Vælg en batterisensor, som den automatiske registrering overså — den kommer straks på listen.",
"battery_fleet_track_self": "Spor selvopladende batterier",
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvopladende enheder vises som genopladelige — ved lavt niveau bedes om opladning, aldrig om nye batterier.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
@@ -6,6 +6,7 @@
"due_soon": "Bald fällig",
"triggered": "Ausgelöst",
"trigger_replaced": "Auslöser ersetzt",
"trigger_removed": "Trigger entfernt",
"ok": "OK",
"all": "Alle",
"new_object": "+ Neues Objekt",
@@ -518,6 +519,7 @@
"group_by_label": "Gruppieren nach",
"state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z.B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.",
"target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).",
"for_minutes_state_help": "0 zählt jede Änderung sofort. Mit Minuten muss der neue Zustand erst so lange bestehen — kurzes Flackern löst dann weder aus noch zählt es.",
"qr_print_title": "QR-Codes drucken",
"qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.",
"qr_print_load": "Objekte laden",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Bei pünktlichem Abschluss.",
"schedule_preview_ends": "(Serie endet)",
"adopt_problem_responsible": "Verantwortliche Person für alle übernommenen Aufgaben (optional)",
"adopt_for_minutes_hint": "Erst auslösen, wenn das Problem so lange besteht — 0 reagiert auf das erste Flackern.",
"adopt_problem_configure": "Konfigurieren",
"history_auto": "Automatisch",
"battery_fleet_title": "Batterie-Flotte",
@@ -838,6 +841,8 @@
"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_add": "Batterie hinzufügen",
"battery_fleet_add_hint": "Einen Batteriesensor wählen, den die automatische Erkennung übersehen hat — er erscheint sofort in der Liste.",
"battery_fleet_track_self": "Selbstladende Batterien mitverfolgen",
"battery_fleet_track_self_hint": "Telefone, Staubsauger und andere selbstladende Geräte erscheinen als Akkus — bei niedrigem Stand wird ans Laden erinnert, nie an neue Zellen.",
"battery_fleet_status_low": "Schwach",
"battery_fleet_status_soon": "Bald",
"battery_fleet_status_ok": "In Ordnung",
@@ -6,6 +6,7 @@
"due_soon": "Due Soon",
"triggered": "Triggered",
"trigger_replaced": "Trigger replaced",
"trigger_removed": "Trigger removed",
"ok": "OK",
"all": "All",
"new_object": "+ New Object",
@@ -524,6 +525,7 @@
"group_by_label": "Group by",
"state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.",
"target_changes_help": "Number of matching transitions before the trigger fires (default: 1).",
"for_minutes_state_help": "0 counts every change immediately. Set minutes and the new state must hold that long first — brief flickers then neither trigger nor count.",
"qr_print_title": "Print QR codes",
"qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.",
"qr_print_load": "Load objects",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Assuming on-time completion.",
"schedule_preview_ends": "(series ends)",
"adopt_problem_responsible": "Responsible user for all adopted tasks (optional)",
"adopt_for_minutes_hint": "Only trigger once the problem has persisted this long — 0 reacts to the first flicker.",
"adopt_problem_configure": "Configure",
"history_auto": "Automatic",
"battery_fleet_title": "Battery fleet",
@@ -838,6 +841,8 @@
"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_add": "Add a battery",
"battery_fleet_add_hint": "Pick a battery sensor the automatic discovery missed — it joins the roster immediately.",
"battery_fleet_track_self": "Track self-charging batteries",
"battery_fleet_track_self_hint": "Phones, vacuums and other devices that recharge themselves appear as rechargeables — a low one asks for a charge, never for new cells.",
"battery_fleet_status_low": "Low",
"battery_fleet_status_soon": "Soon",
"battery_fleet_status_ok": "Healthy",
@@ -517,6 +517,7 @@
"group_by_label": "Agrupar por",
"state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.",
"target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).",
"for_minutes_state_help": "0 cuenta cada cambio de inmediato. Con minutos, el nuevo estado debe mantenerse primero — un parpadeo breve entonces ni dispara ni cuenta.",
"qr_print_title": "Imprimir códigos QR",
"qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.",
"qr_print_load": "Cargar objetos",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "No era necesario",
"quick_complete_success": "Completado rápido",
"trigger_replaced": "Disparador reemplazado",
"trigger_removed": "Disparador eliminado",
"add": "Añadir",
"show_stats": "Mostrar estadísticas + gráficos",
"hide_stats": "Ocultar estadísticas",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Suponiendo finalización puntual.",
"schedule_preview_ends": "(la serie termina)",
"adopt_problem_responsible": "Usuario responsable para todas las tareas adoptadas (opcional)",
"adopt_for_minutes_hint": "Disparar solo si el problema persiste este tiempo — 0 reacciona al primer parpadeo.",
"adopt_problem_configure": "Configurar",
"history_auto": "Automático",
"battery_fleet_title": "Flota de baterías",
@@ -838,6 +841,8 @@
"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_add": "Añadir una batería",
"battery_fleet_add_hint": "Elige un sensor de batería que la detección automática pasó por alto — se une a la lista de inmediato.",
"battery_fleet_track_self": "Seguir baterías de carga automática",
"battery_fleet_track_self_hint": "Teléfonos, aspiradoras y otros dispositivos que se recargan solos aparecen como recargables: si están bajos piden una carga, nunca pilas nuevas.",
"battery_fleet_status_low": "Baja",
"battery_fleet_status_soon": "Pronto",
"battery_fleet_status_ok": "Correcta",
@@ -6,6 +6,7 @@
"due_soon": "Pian erääntyy",
"triggered": "Laukaistu",
"trigger_replaced": "Laukaisin korvattu",
"trigger_removed": "Laukaisin poistettu",
"ok": "OK",
"all": "Kaikki",
"new_object": "+ Uusi kohde",
@@ -518,6 +519,7 @@
"group_by_label": "Ryhmittele",
"state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.",
"target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).",
"for_minutes_state_help": "0 laskee jokaisen muutoksen heti. Minuuteilla uuden tilan on ensin pysyttävä — lyhyet välähdykset eivät silloin laukaise eivätkä kirjaudu.",
"qr_print_title": "Tulosta QR-koodit",
"qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.",
"qr_print_load": "Lataa kohteet",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Olettaen, että valmistuminen on ajallaan.",
"schedule_preview_ends": "(sarja päättyy)",
"adopt_problem_responsible": "Vastuuhenkilö kaikille omaksutuille tehtäville (valinnainen)",
"adopt_for_minutes_hint": "Laukaise vasta kun ongelma on kestänyt näin kauan — 0 reagoi ensimmäiseen välähdykseen.",
"adopt_problem_configure": "Määritä",
"history_auto": "Automaattinen",
"battery_fleet_title": "Paristokanta",
@@ -838,6 +841,8 @@
"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_add": "Lisää paristo",
"battery_fleet_add_hint": "Valitse paristoanturi, jonka automaattinen tunnistus ohitti — se liittyy luetteloon heti.",
"battery_fleet_track_self": "Seuraa itselatautuvia akkuja",
"battery_fleet_track_self_hint": "Puhelimet, imurit ja muut itselatautuvat laitteet näkyvät ladattavina — vähissä oleva pyytää latausta, ei koskaan uusia paristoja.",
"battery_fleet_status_low": "Vähissä",
"battery_fleet_status_soon": "Pian",
"battery_fleet_status_ok": "Kunnossa",
@@ -517,6 +517,7 @@
"group_by_label": "Grouper par",
"state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p.ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.",
"target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut: 1).",
"for_minutes_state_help": "0 compte chaque changement immédiatement. Avec des minutes, le nouvel état doit d'abord se maintenir — un bref clignotement ne déclenche ni ne compte alors.",
"qr_print_title": "Imprimer les QR codes",
"qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.",
"qr_print_load": "Charger les objets",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Non nécessaire",
"quick_complete_success": "Terminé rapidement",
"trigger_replaced": "Déclencheur remplacé",
"trigger_removed": "Déclencheur supprimé",
"add": "Ajouter",
"show_stats": "Afficher les statistiques + graphiques",
"hide_stats": "Masquer les statistiques",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "En supposant un achèvement ponctuel.",
"schedule_preview_ends": "(fin de la série)",
"adopt_problem_responsible": "Personne responsable pour toutes les tâches adoptées (optionnel)",
"adopt_for_minutes_hint": "Ne déclencher que si le problème persiste aussi longtemps — 0 réagit au premier clignotement.",
"adopt_problem_configure": "Configurer",
"history_auto": "Automatique",
"battery_fleet_title": "Parc de piles",
@@ -838,6 +841,8 @@
"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_add": "Ajouter une batterie",
"battery_fleet_add_hint": "Choisissez un capteur de batterie que la détection automatique a manqué — il rejoint la liste immédiatement.",
"battery_fleet_track_self": "Suivre les batteries à recharge automatique",
"battery_fleet_track_self_hint": "Les téléphones, aspirateurs et autres appareils qui se rechargent seuls apparaissent comme rechargeables — un niveau bas demande une recharge, jamais de nouvelles piles.",
"battery_fleet_status_low": "Faible",
"battery_fleet_status_soon": "Bientôt",
"battery_fleet_status_ok": "Bon état",
@@ -6,6 +6,7 @@
"due_soon": "जल्द देय",
"triggered": "ट्रिगर हुआ",
"trigger_replaced": "ट्रिगर बदला गया",
"trigger_removed": "ट्रिगर हटाया गया",
"ok": "ठीक",
"all": "सभी",
"new_object": "+ नई वस्तु",
@@ -518,6 +519,7 @@
"group_by_label": "इसके अनुसार समूहित करें",
"state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।",
"target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।",
"for_minutes_state_help": "0 हर परिवर्तन को तुरंत गिनता है। मिनट सेट करने पर नई स्थिति को पहले उतनी देर बनी रहनी होगी — क्षणिक झिलमिलाहट तब न ट्रिगर करती है न गिनी जाती है।",
"qr_print_title": "QR कोड प्रिंट करें",
"qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।",
"qr_print_load": "वस्तुएँ लोड करें",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "समय पर पूर्णता मानते हुए।",
"schedule_preview_ends": "(श्रृंखला समाप्त)",
"adopt_problem_responsible": "सभी अपनाए गए कार्यों के लिए ज़िम्मेदार उपयोगकर्ता (वैकल्पिक)",
"adopt_for_minutes_hint": "समस्या इतनी देर बनी रहने पर ही ट्रिगर करें — 0 पहली झिलमिलाहट पर प्रतिक्रिया करता है।",
"adopt_problem_configure": "कॉन्फ़िगर करें",
"history_auto": "स्वचालित",
"battery_fleet_title": "बैटरी फ्लीट",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "किसी डिवाइस को यहाँ कम बैटरी बताने से पहले ही बाहर रखें — जैसे खुद चार्ज होने वाला वैक्यूम, या खुद ही चेतावनी देने वाला फ़ोन।",
"battery_fleet_add": "बैटरी जोड़ें",
"battery_fleet_add_hint": "स्वचालित खोज से छूटा बैटरी सेंसर चुनें — यह तुरंत सूची में जुड़ जाएगा।",
"battery_fleet_track_self": "स्वयं चार्ज होने वाली बैटरियों को ट्रैक करें",
"battery_fleet_track_self_hint": "फ़ोन, वैक्यूम और अन्य स्वयं चार्ज होने वाले उपकरण रिचार्जेबल के रूप में दिखते हैं — कम होने पर चार्ज करने को कहा जाता है, नई बैटरी कभी नहीं।",
"battery_fleet_status_low": "कम",
"battery_fleet_status_soon": "जल्द",
"battery_fleet_status_ok": "ठीक",
@@ -6,6 +6,7 @@
"due_soon": "Hamarosan esedékes",
"triggered": "Aktiválva",
"trigger_replaced": "Kiváltó lecserélve",
"trigger_removed": "Eseményindító eltávolítva",
"ok": "OK",
"all": "Összes",
"new_object": "+ Új objektum",
@@ -524,6 +525,7 @@
"group_by_label": "Csoportosítás",
"state_value_help": "A HA állapotértékét használja (általában kisbetűs, pl. „on”/„off”). A kis- és nagybetűk mentéskor normalizálódnak.",
"target_changes_help": "Az egyező átmenetek száma a kiváltás előtt (alapértelmezés: 1).",
"for_minutes_state_help": "A 0 minden változást azonnal számol. Percekkel az új állapotnak előbb fenn kell maradnia — a rövid villanás ekkor se nem indít, se nem számít.",
"qr_print_title": "QR-kódok nyomtatása",
"qr_print_desc": "Nyomtatható oldal QR-kódokkal — vágja ki és ragassza fel a berendezésekre.",
"qr_print_load": "Objektumok betöltése",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Időben történő elvégzést feltételezve.",
"schedule_preview_ends": "(a sorozat véget ér)",
"adopt_problem_responsible": "Felelős felhasználó az összes átvett feladathoz (opcionális)",
"adopt_for_minutes_hint": "Csak akkor induljon, ha a probléma ennyi ideig fennáll — a 0 az első villanásra reagál.",
"adopt_problem_configure": "Beállítás",
"history_auto": "Automatikus",
"battery_fleet_title": "Elemflotta",
@@ -838,6 +841,8 @@
"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_add": "Elem hozzáadása",
"battery_fleet_add_hint": "Válassz egy elem-érzékelőt, amelyet az automatikus felismerés kihagyott — azonnal felkerül a listára.",
"battery_fleet_track_self": "Öntöltő akkumulátorok követése",
"battery_fleet_track_self_hint": "A telefonok, porszívók és más öntöltő eszközök akkumulátorként jelennek meg — alacsony töltöttségnél töltést kérnek, sosem új elemet.",
"battery_fleet_status_low": "Alacsony",
"battery_fleet_status_soon": "Hamarosan",
"battery_fleet_status_ok": "Rendben",
@@ -517,6 +517,7 @@
"group_by_label": "Raggruppa per",
"state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.",
"target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).",
"for_minutes_state_help": "0 conta ogni cambiamento subito. Con i minuti, il nuovo stato deve prima mantenersi — un breve sfarfallio allora non attiva né conta.",
"qr_print_title": "Stampa codici QR",
"qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.",
"qr_print_load": "Carica oggetti",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Non necessario",
"quick_complete_success": "Completato rapidamente",
"trigger_replaced": "Trigger sostituito",
"trigger_removed": "Trigger rimosso",
"add": "Aggiungi",
"show_stats": "Mostra statistiche + grafici",
"hide_stats": "Nascondi statistiche",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Presumendo un completamento puntuale.",
"schedule_preview_ends": "(la serie termina)",
"adopt_problem_responsible": "Utente responsabile per tutte le attività adottate (opzionale)",
"adopt_for_minutes_hint": "Attiva solo se il problema persiste per questo tempo — 0 reagisce al primo sfarfallio.",
"adopt_problem_configure": "Configura",
"history_auto": "Automatico",
"battery_fleet_title": "Parco batterie",
@@ -838,6 +841,8 @@
"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_add": "Aggiungi una batteria",
"battery_fleet_add_hint": "Scegli un sensore batteria sfuggito al rilevamento automatico — entra subito nell'elenco.",
"battery_fleet_track_self": "Traccia le batterie autoricaricabili",
"battery_fleet_track_self_hint": "Telefoni, aspirapolvere e altri dispositivi che si ricaricano da soli appaiono come ricaricabili — se scarichi chiedono una ricarica, mai batterie nuove.",
"battery_fleet_status_low": "Scarica",
"battery_fleet_status_soon": "A breve",
"battery_fleet_status_ok": "A posto",
@@ -6,6 +6,7 @@
"due_soon": "期限間近",
"triggered": "トリガー発生",
"trigger_replaced": "トリガー解除済み",
"trigger_removed": "トリガーを削除しました",
"ok": "OK",
"all": "すべて",
"new_object": "+ 新規対象",
@@ -518,6 +519,7 @@
"group_by_label": "グループ化",
"state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。",
"target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。",
"for_minutes_state_help": "0 はすべての変化を即座にカウントします。分を設定すると新しい状態がその時間続く必要があり、一瞬の変動はトリガーもカウントもされません。",
"qr_print_title": "QRコードを印刷",
"qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。",
"qr_print_load": "対象を読み込む",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "期日どおりに完了した場合。",
"schedule_preview_ends": "(シリーズ終了)",
"adopt_problem_responsible": "採用したすべてのタスクの担当ユーザー(任意)",
"adopt_for_minutes_hint": "問題がこの時間続いた場合にのみトリガーします。0 は最初の変動に即反応します。",
"adopt_problem_configure": "設定",
"history_auto": "自動",
"battery_fleet_title": "電池フリート",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "電池残量が少ないと報告される前に、ここで機器を除外できます — 自動充電するロボット掃除機や、自分で警告するスマートフォンなど。",
"battery_fleet_add": "電池を追加",
"battery_fleet_add_hint": "自動検出が見逃した電池センサーを選択すると、すぐに一覧に追加されます。",
"battery_fleet_track_self": "自己充電式バッテリーを追跡",
"battery_fleet_track_self_hint": "スマートフォンや掃除機など自己充電するデバイスは充電式として表示されます。残量が少ないときは充電を促し、電池交換は求めません。",
"battery_fleet_status_low": "残量低下",
"battery_fleet_status_soon": "まもなく",
"battery_fleet_status_ok": "良好",
@@ -6,6 +6,7 @@
"due_soon": "기한 임박",
"triggered": "트리거됨",
"trigger_replaced": "트리거 교체됨",
"trigger_removed": "트리거 제거됨",
"ok": "정상",
"all": "전체",
"new_object": "+ 새 객체",
@@ -524,6 +525,7 @@
"group_by_label": "그룹화",
"state_value_help": "HA 상태 값을 사용하세요(보통 소문자, 예: \"on\"/\"off\"). 대소문자는 저장 시 정규화됩니다.",
"target_changes_help": "트리거가 발동하기까지 필요한 일치 전환 횟수입니다 (기본값: 1).",
"for_minutes_state_help": "0은 모든 변화를 즉시 집계합니다. 분을 설정하면 새 상태가 먼저 그 시간 동안 유지되어야 하며, 짧은 깜빡임은 트리거되지도 집계되지도 않습니다.",
"qr_print_title": "QR 코드 인쇄",
"qr_print_desc": "잘라서 장비에 붙일 수 있는 QR 코드 인쇄 페이지를 생성합니다.",
"qr_print_load": "객체 불러오기",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "제때 완료한다고 가정한 일정입니다.",
"schedule_preview_ends": "(시리즈 종료)",
"adopt_problem_responsible": "가져온 모든 작업의 담당자 (선택)",
"adopt_for_minutes_hint": "문제가 이 시간 동안 지속된 경우에만 트리거합니다. 0은 첫 깜빡임에 즉시 반응합니다.",
"adopt_problem_configure": "구성",
"history_auto": "자동",
"battery_fleet_title": "배터리 플릿",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "배터리 부족을 알리기 전에 여기에서 기기를 제외하세요 — 스스로 충전하는 로봇청소기나 자체적으로 경고하는 휴대폰처럼.",
"battery_fleet_add": "배터리 추가",
"battery_fleet_add_hint": "자동 검색이 놓친 배터리 센서를 선택하면 즉시 목록에 추가됩니다.",
"battery_fleet_track_self": "자체 충전 배터리 추적",
"battery_fleet_track_self_hint": "휴대폰, 청소기 등 스스로 충전되는 기기는 충전식으로 표시됩니다. 잔량이 낮으면 충전을 요청하며 새 전지를 요구하지 않습니다.",
"battery_fleet_status_low": "부족",
"battery_fleet_status_soon": "곧",
"battery_fleet_status_ok": "정상",
@@ -6,6 +6,7 @@
"due_soon": "Forfaller snart",
"triggered": "Utløst",
"trigger_replaced": "Utløser erstattet",
"trigger_removed": "Utløser fjernet",
"ok": "OK",
"all": "Alle",
"new_object": "+ Nytt objekt",
@@ -518,6 +519,7 @@
"group_by_label": "Grupper etter",
"state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.",
"target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).",
"for_minutes_state_help": "0 teller hver endring umiddelbart. Med minutter må den nye tilstanden først holde seg — korte blink verken utløser eller telles da.",
"qr_print_title": "Skriv ut QR-koder",
"qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.",
"qr_print_load": "Last objekter",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Forutsatt fullføring i tide.",
"schedule_preview_ends": "(serien avsluttes)",
"adopt_problem_responsible": "Ansvarlig bruker for alle overtatte oppgaver (valgfritt)",
"adopt_for_minutes_hint": "Utløs først når problemet har vart så lenge — 0 reagerer på første blink.",
"adopt_problem_configure": "Konfigurer",
"history_auto": "Automatisk",
"battery_fleet_title": "Batteriflåte",
@@ -838,6 +841,8 @@
"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_add": "Legg til et batteri",
"battery_fleet_add_hint": "Velg en batterisensor som den automatiske gjenkjenningen overså — den blir med i listen umiddelbart.",
"battery_fleet_track_self": "Spor selvladende batterier",
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvladende enheter vises som oppladbare — ved lavt nivå bes det om lading, aldri om nye batterier.",
"battery_fleet_status_low": "Lav",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I orden",
@@ -517,6 +517,7 @@
"group_by_label": "Groeperen op",
"state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.",
"target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).",
"for_minutes_state_help": "0 telt elke wijziging direct. Met minuten moet de nieuwe status eerst zo lang aanhouden — kort flikkeren triggert dan niet en telt niet mee.",
"qr_print_title": "QR-codes afdrukken",
"qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.",
"qr_print_load": "Objecten laden",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Niet nodig",
"quick_complete_success": "Snel als voltooid gemarkeerd",
"trigger_replaced": "Trigger vervangen",
"trigger_removed": "Trigger verwijderd",
"add": "Toevoegen",
"show_stats": "Statistieken + grafieken tonen",
"hide_stats": "Statistieken verbergen",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Uitgaande van tijdige afronding.",
"schedule_preview_ends": "(reeks eindigt)",
"adopt_problem_responsible": "Verantwoordelijke gebruiker voor alle overgenomen taken (optioneel)",
"adopt_for_minutes_hint": "Pas triggeren als het probleem zo lang aanhoudt — 0 reageert op de eerste flikkering.",
"adopt_problem_configure": "Configureren",
"history_auto": "Automatisch",
"battery_fleet_title": "Batterijvloot",
@@ -838,6 +841,8 @@
"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_add": "Batterij toevoegen",
"battery_fleet_add_hint": "Kies een batterijsensor die de automatische detectie heeft gemist — hij verschijnt direct in de lijst.",
"battery_fleet_track_self": "Zelfopladende batterijen volgen",
"battery_fleet_track_self_hint": "Telefoons, stofzuigers en andere zelfopladende apparaten verschijnen als oplaadbaar — bij een lage stand wordt om opladen gevraagd, nooit om nieuwe batterijen.",
"battery_fleet_status_low": "Laag",
"battery_fleet_status_soon": "Binnenkort",
"battery_fleet_status_ok": "In orde",
@@ -517,6 +517,7 @@
"group_by_label": "Grupuj wg",
"state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.",
"target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).",
"for_minutes_state_help": "0 liczy każdą zmianę od razu. Z minutami nowy stan musi się najpierw utrzymać — krótkie mignięcia wtedy nie wyzwalają i nie liczą się.",
"qr_print_title": "Drukuj kody QR",
"qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.",
"qr_print_load": "Załaduj obiekty",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Nie było potrzebne",
"quick_complete_success": "Szybko oznaczono jako wykonane",
"trigger_replaced": "Wyzwalacz zastąpiony",
"trigger_removed": "Wyzwalacz usunięty",
"add": "Dodaj",
"show_stats": "Pokaż statystyki + wykresy",
"hide_stats": "Ukryj statystyki",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Przy założeniu terminowego ukończenia.",
"schedule_preview_ends": "(seria się kończy)",
"adopt_problem_responsible": "Osoba odpowiedzialna za wszystkie przejęte zadania (opcjonalnie)",
"adopt_for_minutes_hint": "Wyzwalaj dopiero, gdy problem utrzymuje się tak długo — 0 reaguje na pierwsze mignięcie.",
"adopt_problem_configure": "Konfiguruj",
"history_auto": "Automatycznie",
"battery_fleet_title": "Flota baterii",
@@ -838,6 +841,8 @@
"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_add": "Dodaj baterię",
"battery_fleet_add_hint": "Wybierz czujnik baterii pominięty przez automatyczne wykrywanie — od razu trafi na listę.",
"battery_fleet_track_self": "Śledź baterie samoładujące",
"battery_fleet_track_self_hint": "Telefony, odkurzacze i inne samoładujące się urządzenia pojawiają się jako akumulatory — przy niskim poziomie proszą o ładowanie, nigdy o nowe baterie.",
"battery_fleet_status_low": "Niski",
"battery_fleet_status_soon": "Wkrótce",
"battery_fleet_status_ok": "W porządku",
@@ -6,6 +6,7 @@
"due_soon": "Vence em breve",
"triggered": "Acionada",
"trigger_replaced": "Gatilho substituído",
"trigger_removed": "Gatilho removido",
"ok": "OK",
"all": "Todas",
"new_object": "+ Novo objeto",
@@ -524,6 +525,7 @@
"group_by_label": "Agrupar por",
"state_value_help": "Use o valor de estado do HA (geralmente minúsculo, ex.: \"on\"/\"off\"). Maiúsculas e minúsculas são normalizadas ao salvar.",
"target_changes_help": "Número de transições correspondentes antes de o gatilho disparar (padrão: 1).",
"for_minutes_state_help": "0 conta cada mudança imediatamente. Com minutos, o novo estado deve se manter primeiro — uma oscilação breve então não dispara nem conta.",
"qr_print_title": "Imprimir códigos QR",
"qr_print_desc": "Gere uma página imprimível de códigos QR para recortar e colar nos seus equipamentos.",
"qr_print_load": "Carregar objetos",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Supondo conclusão em dia.",
"schedule_preview_ends": "(a série termina)",
"adopt_problem_responsible": "Usuário responsável por todas as tarefas adotadas (opcional)",
"adopt_for_minutes_hint": "Disparar apenas se o problema persistir por esse tempo — 0 reage à primeira oscilação.",
"adopt_problem_configure": "Configurar",
"history_auto": "Automática",
"battery_fleet_title": "Frota de baterias",
@@ -838,6 +841,8 @@
"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_add": "Adicionar uma bateria",
"battery_fleet_add_hint": "Escolha um sensor de bateria que a detecção automática não encontrou — ele entra na lista imediatamente.",
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem uma recarga, nunca pilhas novas.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
@@ -513,6 +513,7 @@
"group_by_label": "Agrupar por",
"state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.",
"target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).",
"for_minutes_state_help": "0 conta cada mudança de imediato. Com minutos, o novo estado tem de se manter primeiro — uma oscilação breve então não dispara nem conta.",
"qr_print_title": "Imprimir códigos QR",
"qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.",
"qr_print_load": "Carregar objetos",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Não era necessário",
"quick_complete_success": "Concluído rapidamente",
"trigger_replaced": "Acionador substituído",
"trigger_removed": "Gatilho removido",
"add": "Adicionar",
"show_stats": "Mostrar estatísticas + gráficos",
"hide_stats": "Ocultar estatísticas",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Assumindo conclusão pontual.",
"schedule_preview_ends": "(a série termina)",
"adopt_problem_responsible": "Utilizador responsável por todas as tarefas adotadas (opcional)",
"adopt_for_minutes_hint": "Disparar apenas se o problema persistir este tempo — 0 reage à primeira oscilação.",
"adopt_problem_configure": "Configurar",
"history_auto": "Automático",
"battery_fleet_title": "Frota de baterias",
@@ -838,6 +841,8 @@
"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_add": "Adicionar uma bateria",
"battery_fleet_add_hint": "Escolha um sensor de bateria que a deteção automática não encontrou — entra de imediato na lista.",
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem um carregamento, nunca pilhas novas.",
"battery_fleet_status_low": "Fraca",
"battery_fleet_status_soon": "Em breve",
"battery_fleet_status_ok": "Em bom estado",
@@ -517,6 +517,7 @@
"group_by_label": "Группировать по",
"state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.",
"target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).",
"for_minutes_state_help": "0 — каждое изменение засчитывается сразу. С минутами новое состояние сначала должно удержаться — короткие сбои тогда не срабатывают и не считаются.",
"qr_print_title": "Печать QR-кодов",
"qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.",
"qr_print_load": "Загрузить объекты",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Не было необходимо",
"quick_complete_success": "Быстро отмечено как выполнено",
"trigger_replaced": "Триггер заменён",
"trigger_removed": "Триггер удалён",
"add": "Добавить",
"show_stats": "Показать статистику + графики",
"hide_stats": "Скрыть статистику",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "При условии своевременного выполнения.",
"schedule_preview_ends": "(серия завершается)",
"adopt_problem_responsible": "Ответственный пользователь для всех принятых задач (необязательно)",
"adopt_for_minutes_hint": "Срабатывать только если проблема сохраняется это время — 0 реагирует на первый сбой.",
"adopt_problem_configure": "Настроить",
"history_auto": "Автоматически",
"battery_fleet_title": "Парк батарей",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "Исключите устройство здесь ещё до того, как оно сообщит о низком заряде — робот-пылесос, который заряжается сам, или телефон, который предупреждает вас сам.",
"battery_fleet_add": "Добавить батарею",
"battery_fleet_add_hint": "Выберите датчик батареи, который автоматическое обнаружение пропустило — он сразу появится в списке.",
"battery_fleet_track_self": "Отслеживать самозаряжающиеся батареи",
"battery_fleet_track_self_hint": "Телефоны, пылесосы и другие самозаряжающиеся устройства отображаются как аккумуляторы — при низком заряде нужна зарядка, а не новые батарейки.",
"battery_fleet_status_low": "Низкий",
"battery_fleet_status_soon": "Скоро",
"battery_fleet_status_ok": "В норме",
@@ -517,6 +517,7 @@
"group_by_label": "Gruppera efter",
"state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.",
"target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).",
"for_minutes_state_help": "0 räknar varje ändring direkt. Med minuter måste det nya tillståndet först hålla i sig — korta blinkningar varken utlöser eller räknas då.",
"qr_print_title": "Skriv ut QR-koder",
"qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.",
"qr_print_load": "Ladda objekt",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Inte nödvändigt",
"quick_complete_success": "Snabbt markerad som klar",
"trigger_replaced": "Utlösare ersatt",
"trigger_removed": "Utlösare borttagen",
"add": "Lägg till",
"show_stats": "Visa statistik + grafer",
"hide_stats": "Dölj statistik",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Förutsatt slutförande i tid.",
"schedule_preview_ends": "(serien avslutas)",
"adopt_problem_responsible": "Ansvarig användare för alla övertagna uppgifter (valfritt)",
"adopt_for_minutes_hint": "Utlös först när problemet kvarstått så här länge — 0 reagerar på första blinkningen.",
"adopt_problem_configure": "Konfigurera",
"history_auto": "Automatiskt",
"battery_fleet_title": "Batteriflotta",
@@ -838,6 +841,8 @@
"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_add": "Lägg till ett batteri",
"battery_fleet_add_hint": "Välj en batterisensor som den automatiska identifieringen missade — den läggs till i listan direkt.",
"battery_fleet_track_self": "Spåra självladdande batterier",
"battery_fleet_track_self_hint": "Telefoner, dammsugare och andra självladdande enheter visas som laddningsbara — vid låg nivå begärs laddning, aldrig nya batterier.",
"battery_fleet_status_low": "Låg",
"battery_fleet_status_soon": "Snart",
"battery_fleet_status_ok": "I ordning",
@@ -6,6 +6,7 @@
"due_soon": "Yaklaşan",
"triggered": "Tetiklendi",
"trigger_replaced": "Tetikleyici değiştirildi",
"trigger_removed": "Tetikleyici kaldırıldı",
"ok": "Tamam",
"all": "Tümü",
"new_object": "+ Yeni Nesne",
@@ -524,6 +525,7 @@
"group_by_label": "Grupla",
"state_value_help": "HA durum değerini kullanın (genellikle küçük harf, örn. \"on\"/\"off\"). Büyük/küçük harf kaydederken normalleştirilir.",
"target_changes_help": "Tetikleyici çalışmadan önceki eşleşen geçiş sayısı (varsayılan: 1).",
"for_minutes_state_help": "0 her değişikliği hemen sayar. Dakika ayarlanırsa yeni durumun önce o kadar sürmesi gerekir — kısa titremeler o zaman ne tetikler ne de sayılır.",
"qr_print_title": "QR kodlarını yazdır",
"qr_print_desc": "Kesip ekipmanınıza yapıştırmak için yazdırılabilir bir QR kodu sayfası oluşturun.",
"qr_print_load": "Nesneleri yükle",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "Zamanında tamamlama varsayılarak.",
"schedule_preview_ends": "(seri sona erer)",
"adopt_problem_responsible": "Devralınan tüm görevler için sorumlu kullanıcı (isteğe bağlı)",
"adopt_for_minutes_hint": "Yalnızca sorun bu süre boyunca devam ederse tetikle — 0 ilk titremeye tepki verir.",
"adopt_problem_configure": "Yapılandır",
"history_auto": "Otomatik",
"battery_fleet_title": "Pil filosu",
@@ -838,6 +841,8 @@
"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_add": "Pil ekle",
"battery_fleet_add_hint": "Otomatik keşfin atladığı bir pil sensörü seçin — hemen listeye katılır.",
"battery_fleet_track_self": "Kendi kendine şarj olan pilleri izle",
"battery_fleet_track_self_hint": "Telefonlar, süpürgeler ve kendi kendine şarj olan diğer cihazlar şarj edilebilir olarak görünür — seviye düşükken şarj istenir, asla yeni pil istenmez.",
"battery_fleet_status_low": "Düşük",
"battery_fleet_status_soon": "Yakında",
"battery_fleet_status_ok": "İyi durumda",
@@ -517,6 +517,7 @@
"group_by_label": "Групувати за",
"state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.",
"target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).",
"for_minutes_state_help": "0 — кожна зміна зараховується одразу. З хвилинами новий стан спершу має утриматися — короткі збої тоді не спрацьовують і не рахуються.",
"qr_print_title": "Друкувати QR-коди",
"qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.",
"qr_print_load": "Завантажити об'єкти",
@@ -584,6 +585,7 @@
"quick_complete_defaults_feedback_not_needed": "Не було необхідно",
"quick_complete_success": "Швидко позначено виконаним",
"trigger_replaced": "Тригер замінено",
"trigger_removed": "Тригер видалено",
"add": "Додати",
"show_stats": "Показати статистику + графіки",
"hide_stats": "Сховати статистику",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "За умови вчасного виконання.",
"schedule_preview_ends": "(серія завершується)",
"adopt_problem_responsible": "Відповідальний користувач для всіх прийнятих завдань (необов'язково)",
"adopt_for_minutes_hint": "Спрацьовувати лише якщо проблема триває цей час — 0 реагує на перший збій.",
"adopt_problem_configure": "Налаштувати",
"history_auto": "Автоматично",
"battery_fleet_title": "Парк батарей",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "Вилучіть пристрій тут ще до того, як він повідомить про низький заряд — робот-пилосос, що заряджається сам, або телефон, який попереджає сам.",
"battery_fleet_add": "Додати батарею",
"battery_fleet_add_hint": "Виберіть датчик батареї, який автоматичне виявлення пропустило — він одразу з'явиться у списку.",
"battery_fleet_track_self": "Відстежувати самозарядні батареї",
"battery_fleet_track_self_hint": "Телефони, пилососи та інші самозарядні пристрої відображаються як акумулятори — за низького заряду потрібна зарядка, а не нові батарейки.",
"battery_fleet_status_low": "Низький",
"battery_fleet_status_soon": "Незабаром",
"battery_fleet_status_ok": "У нормі",
@@ -6,6 +6,7 @@
"due_soon": "即将到期",
"triggered": "已触发",
"trigger_replaced": "触发器已更换",
"trigger_removed": "触发器已移除",
"ok": "正常",
"all": "全部",
"new_object": "+ 新建维护项",
@@ -518,6 +519,7 @@
"group_by_label": "分组依据",
"state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。",
"target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。",
"for_minutes_state_help": "0 表示每次变化立即计数。设置分钟后,新状态需先保持该时长——短暂闪变既不触发也不计数。",
"qr_print_title": "打印二维码",
"qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。",
"qr_print_load": "加载设备",
@@ -819,6 +821,7 @@
"schedule_preview_ontime": "假设按时完成。",
"schedule_preview_ends": "(系列结束)",
"adopt_problem_responsible": "所有采用任务的负责用户(可选)",
"adopt_for_minutes_hint": "问题持续此时长后才触发——0 表示对首次闪变立即响应。",
"adopt_problem_configure": "配置",
"history_auto": "自动",
"battery_fleet_title": "电池群",
@@ -838,6 +841,8 @@
"battery_fleet_all_hint": "在设备报告电量不足之前就可以在这里将其排除——例如会自行充电的扫地机器人,或本身就会提醒你的手机。",
"battery_fleet_add": "添加电池",
"battery_fleet_add_hint": "选择自动发现遗漏的电池传感器——它会立即加入列表。",
"battery_fleet_track_self": "跟踪自充电电池",
"battery_fleet_track_self_hint": "手机、吸尘器等自充电设备显示为可充电电池——电量低时提示充电,而不是更换电池。",
"battery_fleet_status_low": "电量低",
"battery_fleet_status_soon": "即将",
"battery_fleet_status_ok": "正常",
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.61.0 */
var S="2.61.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();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 v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,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/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(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 d of Array.from(i))t.push(d)}return null}function k(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 b(){try{let t=R("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=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;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"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?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 h(){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}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
/*! maintenance_supporter frontend 2.63.1 */
var S="2.63.1";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();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 v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,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/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(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 d of Array.from(i))t.push(d)}return null}function k(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 b(){try{let t=R("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=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;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"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?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 h(){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}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
@@ -1,42 +1,42 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-TGQTH4TY.js";import{a as d}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JPOJFRHB.js";import{a,b as _,c as t,f as l,g as h,i as g,j as n,n as i,o as v,q as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";var r=class extends h{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._users=[];this._responsible="";this._localeReady=!1;this._userService=null;this._toggle=s=>{let o=new Set(this._selected);o.has(s)?o.delete(s):o.add(s),this._selected=o};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(s=>s.entity_id))};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let s=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:e.suggested_entry_id??void 0,object_name:e.suggested_object_name,device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0})),o=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/adopt",selections:s});this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:o})),this._open=!1}catch(s){this._error=d(s,this._lang)}finally{this._adopting=!1}}}}get _lang(){return v(this.hass)}updated(s){s.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,u(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._responsible="";try{this._userService?this._userService.updateHass(this.hass):this._userService=new m(this.hass);let[s,o]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/discover"}),this._userService.getUsers().catch(()=>[])]);this._sensors=s.sensors||[],this._selected=new Set(this._sensors.map(e=>e.entity_id)),this._users=o}catch(s){this._error=d(s,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return t``;let s=this._lang,o=this._sensors.length>0&&this._selected.size===this._sensors.length;return t`
/*! maintenance_supporter frontend 2.63.1 */
import{a as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-BBM5E3UT.js";import{a as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGUFLW7I.js";import{a as o,b as _,c as i,f as l,g as h,k as g,l as n,p as t,r as u,t as v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";var r=class extends h{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._users=[];this._responsible="";this._forMinutes="0";this._localeReady=!1;this._userService=null;this._toggle=s=>{let a=new Set(this._selected);a.has(s)?a.delete(s):a.add(s),this._selected=a};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(s=>s.entity_id))};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let s=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:e.suggested_entry_id??void 0,object_name:e.suggested_object_name,device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0,for_minutes:parseInt(this._forMinutes,10)>0?parseInt(this._forMinutes,10):void 0})),a=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/adopt",selections:s});this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:a})),this._open=!1}catch(s){this._error=p(s,this._lang)}finally{this._adopting=!1}}}}get _lang(){return u(this.hass)}updated(s){s.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,v(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._responsible="",this._forMinutes="0";try{this._userService?this._userService.updateHass(this.hass):this._userService=new f(this.hass);let[s,a]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/discover"}),this._userService.getUsers().catch(()=>[])]);this._sensors=s.sensors||[],this._selected=new Set(this._sensors.map(e=>e.entity_id)),this._users=a}catch(s){this._error=p(s,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return i``;let s=this._lang,a=this._sensors.length>0&&this._selected.size===this._sensors.length;return i`
<div class="overlay" @click=${this._close}>
<div class="card" @click=${e=>e.stopPropagation()}>
<div class="title">${i("adopt_problem_title",s)}</div>
<div class="hint">${i("adopt_problem_hint",s)}</div>
${this._error?t`<div class="error">${this._error}</div>`:l}
<div class="title">${t("adopt_problem_title",s)}</div>
<div class="hint">${t("adopt_problem_hint",s)}</div>
${this._error?i`<div class="error">${this._error}</div>`:l}
${this._loading?t`<div class="loading">…</div>`:this._sensors.length===0?t`<div class="empty">${i("adopt_problem_none",s)}</div>`:t`
${this._loading?i`<div class="loading">…</div>`:this._sensors.length===0?i`<div class="empty">${t("adopt_problem_none",s)}</div>`:i`
<label class="select-all">
<input
type="checkbox"
.checked=${o}
.checked=${a}
@change=${this._toggleAll}
/>
<span>${i("selected",s)}: ${this._selected.size} / ${this._sensors.length}</span>
<span>${t("selected",s)}: ${this._selected.size} / ${this._sensors.length}</span>
</label>
<div class="list">
${this._sensors.map(e=>{let f=this._selected.has(e.entity_id),p=e.state==="on",c=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return t`
${this._sensors.map(e=>{let m=this._selected.has(e.entity_id),d=e.state==="on",c=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return i`
<label class="row">
<input
type="checkbox"
.checked=${f}
.checked=${m}
@change=${()=>this._toggle(e.entity_id)}
/>
<div class="row-main">
<div class="row-top">
<span class="row-name">${e.name}</span>
<span class="chip ${p?"chip-active":"chip-ok"}">
${p?i("adopt_problem_active",s):i("adopt_problem_ok",s)}
<span class="chip ${d?"chip-active":"chip-ok"}">
${d?t("adopt_problem_active",s):t("adopt_problem_ok",s)}
</span>
</div>
${c?t`<div class="row-sub">${c}</div>`:l}
${c?i`<div class="row-sub">${c}</div>`:l}
<div class="row-target">
${e.suggested_object_name}${e.suggested_entry_id?l:t` <span class="new-tag">${i("adopt_problem_new_object",s)}</span>`}
${e.suggested_object_name}${e.suggested_entry_id?l:i` <span class="new-tag">${t("adopt_problem_new_object",s)}</span>`}
</div>
${e.suggested_part_name?t`<div class="row-part">
${e.suggested_part_name?i`<div class="row-part">
<ha-icon icon="mdi:package-variant-closed"></ha-icon>
${i("adopt_problem_part",s).replace("{name}",e.suggested_part_name)}
${t("adopt_problem_part",s).replace("{name}",e.suggested_part_name)}
</div>`:l}
</div>
</label>
@@ -44,28 +44,43 @@ import{a as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-TGQTH4TY
</div>
`}
${!this._loading&&this._sensors.length>0&&this._users.length>0?t`
${!this._loading&&this._sensors.length>0?i`
<label class="responsible">
<span>${i("adopt_problem_responsible",s)}</span>
<span>${t("for_at_least_minutes",s)}</span>
<input
class="for-input"
type="number"
min="0"
max="1440"
.value=${this._forMinutes}
@input=${e=>this._forMinutes=e.target.value}
/>
</label>
<div class="for-hint">${t("adopt_for_minutes_hint",s)}</div>
`:l}
${!this._loading&&this._sensors.length>0&&this._users.length>0?i`
<label class="responsible">
<span>${t("adopt_problem_responsible",s)}</span>
<select
.value=${this._responsible}
@change=${e=>{this._responsible=e.target.value}}
>
<option value="" ?selected=${!this._responsible}>${i("no_user_assigned",s)}</option>
${this._users.map(e=>t`<option value=${e.id} ?selected=${e.id===this._responsible}>${e.name}</option>`)}
<option value="" ?selected=${!this._responsible}>${t("no_user_assigned",s)}</option>
${this._users.map(e=>i`<option value=${e.id} ?selected=${e.id===this._responsible}>${e.name}</option>`)}
</select>
</label>
`:l}
<div class="actions">
<ha-button appearance="plain" @click=${this._close}>
${i("cancel",s)}
${t("cancel",s)}
</ha-button>
<ha-button
@click=${this._adopt}
.disabled=${this._selected.size===0||this._adopting}
>
${i("adopt_problem_adopt",s)}
${t("adopt_problem_adopt",s)}
</ha-button>
</div>
</div>
@@ -203,6 +218,19 @@ import{a as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-TGQTH4TY
color: var(--secondary-text-color);
flex-wrap: wrap;
}
.for-input {
width: 70px;
background: var(--card-background-color);
color: var(--primary-text-color);
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 4px 6px;
}
.for-hint {
font-size: 11px;
color: var(--secondary-text-color);
margin: -4px 0 2px;
}
.responsible select {
flex: 1;
min-width: 140px;
@@ -219,4 +247,4 @@ import{a as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-TGQTH4TY
gap: 8px;
padding-top: 8px;
}
`,a([g({attribute:!1})],r.prototype,"hass",2),a([n()],r.prototype,"_open",2),a([n()],r.prototype,"_loading",2),a([n()],r.prototype,"_adopting",2),a([n()],r.prototype,"_error",2),a([n()],r.prototype,"_sensors",2),a([n()],r.prototype,"_selected",2),a([n()],r.prototype,"_users",2),a([n()],r.prototype,"_responsible",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",r);export{r as MaintenanceAdoptProblemSensorsDialog};
`,o([g({attribute:!1})],r.prototype,"hass",2),o([n()],r.prototype,"_open",2),o([n()],r.prototype,"_loading",2),o([n()],r.prototype,"_adopting",2),o([n()],r.prototype,"_error",2),o([n()],r.prototype,"_sensors",2),o([n()],r.prototype,"_selected",2),o([n()],r.prototype,"_users",2),o([n()],r.prototype,"_responsible",2),o([n()],r.prototype,"_forMinutes",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",r);export{r as MaintenanceAdoptProblemSensorsDialog};
@@ -1,2 +0,0 @@
/*! maintenance_supporter frontend 2.61.0 */
function m(n,t,e){let o=new Blob([n],{type:e}),r=URL.createObjectURL(o),a=document.createElement("a");a.href=r,a.download=t,a.target="_blank",a.rel="noopener",a.style.display="none",document.body.appendChild(a),a.dispatchEvent(new MouseEvent("click")),document.body.removeChild(a),setTimeout(()=>URL.revokeObjectURL(r),6e4)}function i(n,t){let e=document.createElement("a");e.href=n,e.download=t,e.target="_blank",e.rel="noopener",e.style.display="none",document.body.appendChild(e),e.dispatchEvent(new MouseEvent("click")),document.body.removeChild(e)}async function c(n,t,e=300){return(await n.connection.sendMessagePromise({type:"auth/sign_path",path:t,expires:e})).path}async function s(n,t,e=300){return c(n,`/api/maintenance_supporter/document/${t}`,e)}async function b(n,t,e=""){let o=window.open("about:blank","_blank");try{let r=await s(n,t);o&&(o.location.href=new URL(r+e,window.location.origin).href)}catch(r){throw o&&o.close(),r}}async function g(n,t,e){i(await s(n,t,30),e)}var d=[{key:"name",labelKey:"name",required:!0},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],u=d.map(n=>n.key),l=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function _(n){if(!Array.isArray(n))return[...l];let t=new Set,e=[];for(let o of n)typeof o=="string"&&u.includes(o)&&!t.has(o)&&(t.add(o),e.push(o));return e.length?(e.includes("name")||e.unshift("name"),e):[...l]}export{m as a,i as b,c,s as d,b as e,g as f,d as g,l as h,_ as i};
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.61.0 */
/*! maintenance_supporter frontend 2.63.1 */
var r=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=e,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,e,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:e,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}};export{r as a};
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a as _,e as k}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4Q4MHZO7.js";import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JPOJFRHB.js";import{a as s,b,c as a,f as d,g as v,i as n,j as l,n as r,z as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";var i=class extends v{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue="";this._restockQty="";this._completedAt="";this._usedParts={};this.checklistPrefill={}}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((e,t)=>[String(t),!!this.checklistPrefill[e]]).filter(([,e])=>e)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._completedAt="",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[_(e),{...e}])))}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,o=t.files?.[0];if(t.value="",!!o){this._photoUploading=!0,this._error="";try{let c=new FormData;c.append("entry_id",this.entryId),c.append("tags","photo"),c.append("file",o,o.name);let p=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:c});if(!p.ok){this._error=p.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let u=await p.json();u.id&&(this._photoDocId=u.id,this._photoPreview=URL.createObjectURL(o))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._completedAt){if(new Date(this._completedAt).getTime()>Date.now()){this._error=r("completed_at_future_error",this.lang),this._loading=!1;return}e.completed_at=this._completedAt.length===16?`${this._completedAt}:00`:this._completedAt}if(this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=g(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?a`<span class="req-mark" aria-hidden="true">*</span>`:d}_partsCostSuggestion(){if(this.restockDefault!==null){let o=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(o)||o<=0?null:Math.round(this.restockUnitCost*o*100)/100}if(!this.parts.length)return null;let e=0,t=!1;for(let o of Object.values(this._usedParts)){let c=this.parts.find(p=>_({part_id:p.id,entry_id:p.entry_id})===_(o));c?.cost!=null&&(e+=c.cost*(o.quantity||1),t=!0)}return t?Math.round(e*100)/100:null}_renderCostSuggestion(e){if(this._cost.trim()!=="")return d;let t=this._partsCostSuggestion();if(t==null||t<=0)return d;let o=`${t.toFixed(2)}${this.currencySymbol?` ${this.currencySymbol}`:""}`;return a`<button
/*! maintenance_supporter frontend 2.63.1 */
import{a as _,e as k}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VIX7PNOY.js";import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGUFLW7I.js";import{B as m,a as s,b,c as a,f as d,g as v,k as n,l,p as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";var i=class extends v{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue="";this._restockQty="";this._completedAt="";this._usedParts={};this.checklistPrefill={}}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((e,t)=>[String(t),!!this.checklistPrefill[e]]).filter(([,e])=>e)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._completedAt="",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[_(e),{...e}])))}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,o=t.files?.[0];if(t.value="",!!o){this._photoUploading=!0,this._error="";try{let c=new FormData;c.append("entry_id",this.entryId),c.append("tags","photo"),c.append("file",o,o.name);let p=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:c});if(!p.ok){this._error=p.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let u=await p.json();u.id&&(this._photoDocId=u.id,this._photoPreview=URL.createObjectURL(o))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._completedAt){if(new Date(this._completedAt).getTime()>Date.now()){this._error=r("completed_at_future_error",this.lang),this._loading=!1;return}e.completed_at=this._completedAt.length===16?`${this._completedAt}:00`:this._completedAt}if(this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=g(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?a`<span class="req-mark" aria-hidden="true">*</span>`:d}_partsCostSuggestion(){if(this.restockDefault!==null){let o=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(o)||o<=0?null:Math.round(this.restockUnitCost*o*100)/100}if(!this.parts.length)return null;let e=0,t=!1;for(let o of Object.values(this._usedParts)){let c=this.parts.find(p=>_({part_id:p.id,entry_id:p.entry_id})===_(o));c?.cost!=null&&(e+=c.cost*(o.quantity||1),t=!0)}return t?Math.round(e*100)/100:null}_renderCostSuggestion(e){if(this._cost.trim()!=="")return d;let t=this._partsCostSuggestion();if(t==null||t<=0)return d;let o=`${t.toFixed(2)}${this.currencySymbol?` ${this.currencySymbol}`:""}`;return a`<button
type="button"
class="cost-suggestion"
@click=${()=>this._cost=t.toFixed(2)}
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.63.1 */
import{b as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-IMKP65C5.js";async function l(e,n,t=300){return(await e.connection.sendMessagePromise({type:"auth/sign_path",path:n,expires:t})).path}async function r(e,n,t=300){return l(e,`/api/maintenance_supporter/document/${n}`,t)}async function y(e,n,t=""){let a=window.open("about:blank","_blank");try{let o=await r(e,n);a&&(a.location.href=new URL(o+t,window.location.origin).href)}catch(o){throw a&&a.close(),o}}async function p(e,n,t){i(await r(e,n,30),t)}function d(e){let n=URL.createObjectURL(new Blob([e],{type:"text/html"}));window.open(n,"_blank"),setTimeout(()=>URL.revokeObjectURL(n),6e4)}var c=[{key:"name",labelKey:"name",required:!0},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],u=c.map(e=>e.key),s=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function g(e){if(!Array.isArray(e))return[...s];let n=new Set,t=[];for(let a of e)typeof a=="string"&&u.includes(a)&&!n.has(a)&&(n.add(a),t.push(a));return t.length?(t.includes("name")||t.unshift("name"),t):[...s]}export{l as a,r as b,y as c,p as d,d as e,c as f,s as g,g as h};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.63.1 */
function c(o,t,e){let l=new Blob([o],{type:e}),d=URL.createObjectURL(l),n=document.createElement("a");n.href=d,n.download=t,n.target="_blank",n.rel="noopener",n.style.display="none",document.body.appendChild(n),n.dispatchEvent(new MouseEvent("click")),document.body.removeChild(n),setTimeout(()=>URL.revokeObjectURL(d),6e4)}function r(o,t){let e=document.createElement("a");e.href=o,e.download=t,e.target="_blank",e.rel="noopener",e.style.display="none",document.body.appendChild(e),e.dispatchEvent(new MouseEvent("click")),document.body.removeChild(e)}export{c as a,r as b};
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JPOJFRHB.js";import{a as r,b as _,c as l,f as o,g as p,i as d,j as s,n as i,o as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";var e=class extends p{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return h(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(a,n){this._entryId=a,this._name=n.name||"",this._manufacturer=n.manufacturer||"",this._model=n.model||"",this._serialNumber=n.serial_number||"",this._areaId=n.area_id||"",this._installationDate=n.installation_date||"",this._warrantyExpiry=n.warranty_expiry||"",this._documentationUrl=n.documentation_url||"",this._notes=n.notes||"",this._haDeviceId=n.ha_device_id||"",this._parentEntryId=n.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(a){this._error=u(a,this._lang,i("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(a=>a.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let a=this._lang,n=this._entryId?i("edit_object",a):i("new_object",a);return l`
/*! maintenance_supporter frontend 2.63.1 */
import{a as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGUFLW7I.js";import{a as r,b as _,c as l,f as o,g as p,k as d,l as s,p as i,r as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";var e=class extends p{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return h(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(a,n){this._entryId=a,this._name=n.name||"",this._manufacturer=n.manufacturer||"",this._model=n.model||"",this._serialNumber=n.serial_number||"",this._areaId=n.area_id||"",this._installationDate=n.installation_date||"",this._warrantyExpiry=n.warranty_expiry||"",this._documentationUrl=n.documentation_url||"",this._notes=n.notes||"",this._haDeviceId=n.ha_device_id||"",this._parentEntryId=n.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(a){this._error=u(a,this._lang,i("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(a=>a.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let a=this._lang,n=this._entryId?i("edit_object",a):i("new_object",a);return l`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${n}</div>
<div class="content">
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a as t,b as a,c as i,f as l,g as p,i as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";var e=class extends p{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(n){let o=n.target.value;this.value=o,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:o}}))}render(){return i`
/*! maintenance_supporter frontend 2.63.1 */
import{a as t,b as a,c as i,f as l,g as p,k as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";var e=class extends p{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(n){let o=n.target.value;this.value=o,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:o}}))}render(){return i`
<label class="field">
${this.label?i`<span class="label">${this.label}${this.required?i`<span class="req">*</span>`:l}</span>`:l}
<input
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.61.0 */
import{n as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";var s={name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_equals:"trigger_equals",trigger_not_equals:"trigger_not_equals",trigger_for_minutes:"trigger_for_minutes"};function c(r,o){let e=s[r];if(!e)return r;let t=a(e,o);return t&&t!==e?t:r}function d(r){let e=r.match(/data\['([^']+)'\]/)?.[1],t;return(t=r.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=r.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=r.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=r.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(r)?{field:e,rule:"required"}:(t=r.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(r)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(r)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function g(r,o,e){if(e=e??a("action_error",o),typeof r=="string")return r;if(typeof r!="object"||r===null)return e;let t=r,_=t.message||t.error?.message||"";if(!_)return e;let i=d(_),l=i.field?c(i.field,o):"",n=u=>a(u,o).replace("{field}",l).replace("{n}",i.param??"");switch(i.rule){case"too_long":return n("err_too_long");case"too_short":return n("err_too_short");case"value_too_high":return n("err_value_too_high");case"value_too_low":return n("err_value_too_low");case"required":return n("err_required");case"wrong_type":return n("err_wrong_type").replace("{type}",i.param??"");case"invalid_choice":return n("err_invalid_choice");case"invalid_value":return n("err_invalid_value");default:return _||e}}export{g as a};
/*! maintenance_supporter frontend 2.63.1 */
import{p as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";var s={name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_equals:"trigger_equals",trigger_not_equals:"trigger_not_equals",trigger_for_minutes:"trigger_for_minutes"};function c(r,o){let e=s[r];if(!e)return r;let t=a(e,o);return t&&t!==e?t:r}function d(r){let e=r.match(/data\['([^']+)'\]/)?.[1],t;return(t=r.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=r.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=r.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=r.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(r)?{field:e,rule:"required"}:(t=r.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(r)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(r)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function g(r,o,e){if(e=e??a("action_error",o),typeof r=="string")return r;if(typeof r!="object"||r===null)return e;let t=r,_=t.message||t.error?.message||"";if(!_)return e;let i=d(_),l=i.field?c(i.field,o):"",n=u=>a(u,o).replace("{field}",l).replace("{n}",i.param??"");switch(i.rule){case"too_long":return n("err_too_long");case"too_short":return n("err_too_short");case"value_too_high":return n("err_value_too_high");case"value_too_low":return n("err_value_too_low");case"required":return n("err_required");case"wrong_type":return n("err_wrong_type").replace("{type}",i.param??"");case"invalid_choice":return n("err_invalid_choice");case"invalid_value":return n("err_invalid_value");default:return _||e}}export{g as a};
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.61.0 */
import{n as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";function u(e){return`${e.entry_id??""}\0${e.part_id}`}function l(e,r,s,c){let n=!!e.entry_id&&e.entry_id!==r,o=n?e.entry_id:r,a=s.find(p=>p.entry_id===o),t=(a?.parts||[]).find(p=>p.id===e.part_id)||null,d=n&&a?.object?.name||"",i=t?.name||_("shared_part_unknown",c);return{part:t,foreign:n,ownerName:d,label:d?`${i} (${d})`:i}}function P(e,r,s,c){let{part:n,label:o}=l(e,r,s,c),a=n&&n.stock!==null&&n.stock!==void 0?` (${n.stock}${n.unit?" "+n.unit:""})`:"",t=n?.storage_location?` \u2014 ${n.storage_location}`:"";return`${e.quantity}\xD7 ${o}${a}${t}`}function g(e,r,s,c){let o=(s.find(t=>t.entry_id===r)?.parts||[]).map(t=>({...t})),a=new Set(o.map(t=>u({part_id:t.id})));for(let t of e?.consumes_parts||[]){if(!t.entry_id||t.entry_id===r)continue;let d=u(t);if(a.has(d))continue;a.add(d);let{part:i,ownerName:p}=l(t,r,s,c);o.push({id:t.part_id,name:i?.name||_("shared_part_unknown",c),unit:i?.unit,stock:i?.stock??null,storage_location:i?.storage_location,entry_id:t.entry_id,owner_name:p})}return o}var f=["notes","cost","duration","photo","user"],m={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};export{u as a,P as b,g as c,f as d,m as e};
/*! maintenance_supporter frontend 2.63.1 */
import{p as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";function u(e){return`${e.entry_id??""}\0${e.part_id}`}function l(e,r,s,c){let n=!!e.entry_id&&e.entry_id!==r,o=n?e.entry_id:r,a=s.find(p=>p.entry_id===o),t=(a?.parts||[]).find(p=>p.id===e.part_id)||null,d=n&&a?.object?.name||"",i=t?.name||_("shared_part_unknown",c);return{part:t,foreign:n,ownerName:d,label:d?`${i} (${d})`:i}}function P(e,r,s,c){let{part:n,label:o}=l(e,r,s,c),a=n&&n.stock!==null&&n.stock!==void 0?` (${n.stock}${n.unit?" "+n.unit:""})`:"",t=n?.storage_location?` \u2014 ${n.storage_location}`:"";return`${e.quantity}\xD7 ${o}${a}${t}`}function g(e,r,s,c){let o=(s.find(t=>t.entry_id===r)?.parts||[]).map(t=>({...t})),a=new Set(o.map(t=>u({part_id:t.id})));for(let t of e?.consumes_parts||[]){if(!t.entry_id||t.entry_id===r)continue;let d=u(t);if(a.has(d))continue;a.add(d);let{part:i,ownerName:p}=l(t,r,s,c);o.push({id:t.part_id,name:i?.name||_("shared_part_unknown",c),unit:i?.unit,stock:i?.stock??null,storage_location:i?.storage_location,entry_id:t.entry_id,owner_name:p})}return o}var f=["notes","cost","duration","photo","user"],m={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};export{u as a,P as b,g as c,f as d,m as e};
@@ -1,7 +1,7 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a,b as v,c as n,f as g,g as b,i as m,j as c,n as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";function p(l){return l.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function x(l){return!l.startsWith("data:image/svg+xml,")&&!l.startsWith("data:image/png;base64,")?"":p(l)}function $(l){return l.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var r=class extends b{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,i){this._entryId=e,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,i,o,s){this._entryId=e,this._taskId=i,this._objectName=o,this._taskName=s,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let o=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&o.push(this.hass.connection.sendMessagePromise({...i,action:"complete"}));let s=await Promise.all(o);if(e!==this._generateSeq)return;this._viewResult=s[0],s.length>1&&(this._completeResult=s[1])}catch(i){if(e!==this._generateSeq)return;let o=i?.code,s=i?.message;this._error=o==="no_url"||typeof s=="string"&&s.includes("No Home Assistant URL")?t("qr_error_no_url",this.lang):t("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,i=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,o=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),s=window.open("","_blank","width=600,height=500");if(!s)return;let h=this.lang||"en",d=p(i),u=p(o),_=!!this._completeResult,f=p(t("qr_action_view",h)),w=p(t("qr_action_complete",h));s.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
/*! maintenance_supporter frontend 2.63.1 */
import{a as x}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-IMKP65C5.js";import{a,b as v,c as n,f as p,g as b,k as h,l as c,p as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";function d(l){return l.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function f(l){return!l.startsWith("data:image/svg+xml,")&&!l.startsWith("data:image/png;base64,")?"":d(l)}function q(l){return l.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var o=class extends b{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,i){this._entryId=e,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,i,r,s){this._entryId=e,this._taskId=i,this._objectName=r,this._taskName=s,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let r=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&r.push(this.hass.connection.sendMessagePromise({...i,action:"complete"}));let s=await Promise.all(r);if(e!==this._generateSeq)return;this._viewResult=s[0],s.length>1&&(this._completeResult=s[1])}catch(i){if(e!==this._generateSeq)return;let r=i?.code,s=i?.message;this._error=r==="no_url"||typeof s=="string"&&s.includes("No Home Assistant URL")?t("qr_error_no_url",this.lang):t("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,i=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,r=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),s=window.open("","_blank","width=600,height=500");if(!s)return;let g=this.lang||"en",u=d(i),m=d(r),_=!!this._completeResult,w=d(t("qr_action_view",g)),$=d(t("qr_action_complete",g));s.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="color-scheme" content="light">
<title>${d}</title>
<title>${u}</title>
<style>
/* Printable sheet \u2014 must not inherit the phone's dark theme. The QR images
carry their own white quiet zone and stay scannable either way, but the
@@ -17,25 +17,25 @@ import{a,b as v,c as n,f as g,g as b,i as m,j as c,n as t}from"/maintenance_supp
.qr-label{font-size:13px;font-weight:500;color:#333}
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
</style></head><body>
<h2>${d}</h2>
${u?`<div class="sub">${u}</div>`:""}
<h2>${u}</h2>
${m?`<div class="sub">${m}</div>`:""}
<div class="qr-row">
<div class="qr-col">
<img src="${x(this._viewResult.svg_data_uri)}" alt="QR Info" />
<div class="qr-label">${f}</div>
<img src="${f(this._viewResult.svg_data_uri)}" alt="QR Info" />
<div class="qr-label">${w}</div>
</div>
${_?`<div class="qr-col">
<img src="${x(this._completeResult.svg_data_uri)}" alt="QR Complete" />
<div class="qr-label">${w}</div>
<img src="${f(this._completeResult.svg_data_uri)}" alt="QR Complete" />
<div class="qr-label">${$}</div>
</div>`:""}
</div>
<div class="url">${p(this._viewResult.url)}</div>
<div class="url">${d(this._viewResult.url)}</div>
<script>setTimeout(()=>window.print(),300)<\/script>
</body></html>`),s.document.close()}_downloadSvg(e,i){let o=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),s=new Blob([o],{type:"image/svg+xml"}),h=URL.createObjectURL(s),d=document.createElement("a");d.href=h;let u=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;d.download=`qr-${$(u)}-${i}.svg`,d.click(),URL.revokeObjectURL(h)}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en",i=this._taskName?`${t("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${t("qr_code",e)}: ${this._objectName}`,o=!!this._viewResult;return n`
</body></html>`),s.document.close()}_downloadSvg(e,i){let r=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),s=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;x(r,`qr-${q(s)}-${i}.svg`,"image/svg+xml")}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en",i=this._taskName?`${t("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${t("qr_code",e)}: ${this._objectName}`,r=!!this._viewResult;return n`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${i}</div>
<div class="content">
${this._loading?n`<div class="loading">${t("qr_generating",e)}</div>`:this._error?n`<div class="error">${this._error}</div>`:o?n`
${this._loading?n`<div class="loading">${t("qr_generating",e)}</div>`:this._error?n`<div class="error">${this._error}</div>`:r?n`
<div class="qr-pair">
<div class="qr-item">
<img
@@ -64,10 +64,10 @@ ${u?`<div class="sub">${u}</div>`:""}
${t("qr_download",e)}
</button>
</div>
`:g}
`:p}
</div>
<div class="url-display">${this._viewResult.url}</div>
`:g}
`:p}
<div class="action-row">
<label>${t("qr_url_mode",e)}</label>
<div class="action-toggle">
@@ -86,13 +86,13 @@ ${u?`<div class="sub">${u}</div>`:""}
</ha-button>
<ha-button
@click=${this._print}
.disabled=${!o}
.disabled=${!r}
>
${t("qr_print",e)}
</ha-button>
</div>
</ha-dialog>
`}};r.styles=v`
`}};o.styles=v`
.dialog-title {
font-size: 18px;
font-weight: 500;
@@ -210,4 +210,4 @@ ${u?`<div class="sub">${u}</div>`:""}
color: var(--text-primary-color, #fff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
`,a([m({attribute:!1})],r.prototype,"hass",2),a([m()],r.prototype,"lang",2),a([c()],r.prototype,"_open",2),a([c()],r.prototype,"_loading",2),a([c()],r.prototype,"_error",2),a([c()],r.prototype,"_viewResult",2),a([c()],r.prototype,"_completeResult",2),a([c()],r.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",r);export{r as a};
`,a([h({attribute:!1})],o.prototype,"hass",2),a([h()],o.prototype,"lang",2),a([c()],o.prototype,"_open",2),a([c()],o.prototype,"_loading",2),a([c()],o.prototype,"_error",2),a([c()],o.prototype,"_viewResult",2),a([c()],o.prototype,"_completeResult",2),a([c()],o.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",o);export{o as a};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.63.1 */
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FBQXAJIT.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VIX7PNOY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGUFLW7I.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";export{a as MaintenanceCompleteDialog};
@@ -1,2 +0,0 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-P4GKDOK3.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4Q4MHZO7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JPOJFRHB.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";export{a as MaintenanceCompleteDialog};
@@ -1,2 +0,0 @@
/*! maintenance_supporter frontend 2.61.0 */
import{g as a,h as b,i as c,j as d,k as e,l as f,m as g,n as h,o as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-LVFV6HYC.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4LG4SJUR.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-H5KOKMZN.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-EW357W3K.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-P4GKDOK3.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4Q4MHZO7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-TRENHXOS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-TGQTH4TY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JPOJFRHB.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";export{f as openCompleteDialog,a as openCreateObjectDialog,c as openCreateTaskDialog,b as openEditObjectDialog,d as openEditTaskDialog,e as openHistoryEditDialog,i as openObjectQuickActions,g as openQrDialog,h as openTaskQuickActions};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.63.1 */
import{g as a,h as b,i as c,j as d,k as e,l as f,m as g,n as h,o as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NWIQAC7B.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JIB2VNVZ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JY37H466.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-NOHIK75N.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-FBQXAJIT.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VIX7PNOY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VWJA3YLV.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-IMKP65C5.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-BBM5E3UT.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGUFLW7I.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";export{f as openCompleteDialog,a as openCreateObjectDialog,c as openCreateTaskDialog,b as openEditObjectDialog,d as openEditTaskDialog,e as openHistoryEditDialog,i as openObjectQuickActions,g as openQrDialog,h as openTaskQuickActions};
@@ -1,2 +0,0 @@
/*! maintenance_supporter frontend 2.61.0 */
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4LG4SJUR.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-EW357W3K.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JPOJFRHB.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4JER2GPD.js";export{a as MaintenanceObjectDialog};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.63.1 */
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JIB2VNVZ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-NOHIK75N.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGUFLW7I.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";export{a as MaintenanceObjectDialog};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.63.1 */
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VWJA3YLV.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-IMKP65C5.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-FS5TZIDQ.js";export{a as MaintenanceQrDialog};

Some files were not shown because too many files have changed in this diff Show More