From c29f5cc08870d0d0b77aff709ac72af8a224ceec Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Fri, 2 Oct 2026 16:06:24 +0000 Subject: [PATCH] 257 files --- .ha_run.lock | 2 +- .../maintenance_supporter/__init__.py | 52 +- .../de/maintenance_supporter.yaml | 164 +- .../en/maintenance_supporter.yaml | 131 +- .../es/maintenance_supporter.yaml | 129 +- .../fr/maintenance_supporter.yaml | 133 +- .../it/maintenance_supporter.yaml | 129 +- .../nl/maintenance_supporter.yaml | 137 +- .../assist_sentences/responses/cs.json | 48 +- .../assist_sentences/responses/da.json | 48 +- .../assist_sentences/responses/de.json | 48 +- .../assist_sentences/responses/en.json | 48 +- .../assist_sentences/responses/es.json | 48 +- .../assist_sentences/responses/fi.json | 48 +- .../assist_sentences/responses/fr.json | 48 +- .../assist_sentences/responses/hi.json | 48 +- .../assist_sentences/responses/hu.json | 48 +- .../assist_sentences/responses/it.json | 48 +- .../assist_sentences/responses/ja.json | 48 +- .../assist_sentences/responses/ko.json | 48 +- .../assist_sentences/responses/nb.json | 48 +- .../assist_sentences/responses/nl.json | 48 +- .../assist_sentences/responses/pl.json | 48 +- .../assist_sentences/responses/pt-br.json | 48 +- .../assist_sentences/responses/pt.json | 48 +- .../assist_sentences/responses/ru.json | 52 +- .../assist_sentences/responses/sv.json | 48 +- .../assist_sentences/responses/tr.json | 48 +- .../assist_sentences/responses/uk.json | 52 +- .../assist_sentences/responses/zh.json | 48 +- .../maintenance_supporter/binary_sensor.py | 3 +- .../maintenance_supporter/calendar.py | 24 + .../maintenance_supporter/config_flow.py | 26 + .../config_flow_helpers.py | 8 +- .../config_flow_options_global.py | 19 +- .../config_flow_options_task_adaptive.py | 6 +- .../config_flow_options_task_crud.py | 50 +- .../config_flow_options_task_trigger.py | 23 + .../config_flow_trigger.py | 51 + .../maintenance_supporter/const.py | 15 +- .../maintenance_supporter/coordinator.py | 112 +- .../entity/entity_base.py | 12 +- .../entity/triggers/__init__.py | 10 +- .../entity/triggers/base_trigger.py | 77 +- .../entity/triggers/compound.py | 46 +- .../entity/triggers/counter.py | 4 +- .../entity/triggers/runtime.py | 4 +- .../entity/triggers/state_change.py | 4 +- .../entity/triggers/threshold.py | 4 +- .../maintenance_supporter/export.py | 24 +- .../frontend-src/__tests__/_test-utils.ts | 6 +- .../adopt-problem-sensors-dialog.test.ts | 6 +- .../__tests__/complete-dialog.test.ts | 93 +- .../dialog-no-lazy-load-elements.test.ts | 12 +- .../__tests__/group-dialog-sort.test.ts | 2 +- .../__tests__/ll-custom-payload.test.ts | 6 +- .../__tests__/panel-shell.test.ts | 38 + .../suggested-setups-baseline.test.ts | 4 +- .../__tests__/task-detail-renderer.test.ts | 36 +- .../__tests__/task-detail-view.test.ts | 7 +- .../task-dialog-calendar-kinds.test.ts | 2 +- .../task-dialog-runtime-onstates.test.ts | 4 +- .../components/battery-fleet-section.ts | 82 +- .../components/complete-dialog.ts | 80 +- .../frontend-src/components/object-dialog.ts | 152 +- .../frontend-src/components/parts-section.ts | 62 +- .../frontend-src/components/qr-dialog.ts | 19 +- .../frontend-src/components/settings-view.ts | 74 +- .../frontend-src/components/task-dialog.ts | 154 +- .../components/task-quick-actions-dialog.ts | 106 +- .../frontend-src/dialog-mount.ts | 32 +- .../frontend-src/helpers/calendar-bucket.ts | 12 +- .../frontend-src/locales/cs.json | 67 +- .../frontend-src/locales/da.json | 67 +- .../frontend-src/locales/de.json | 67 +- .../frontend-src/locales/en.json | 67 +- .../frontend-src/locales/es.json | 67 +- .../frontend-src/locales/fi.json | 67 +- .../frontend-src/locales/fr.json | 67 +- .../frontend-src/locales/hi.json | 67 +- .../frontend-src/locales/hu.json | 67 +- .../frontend-src/locales/it.json | 67 +- .../frontend-src/locales/ja.json | 67 +- .../frontend-src/locales/ko.json | 67 +- .../frontend-src/locales/nb.json | 67 +- .../frontend-src/locales/nl.json | 67 +- .../frontend-src/locales/pl.json | 67 +- .../frontend-src/locales/pt-br.json | 67 +- .../frontend-src/locales/pt.json | 67 +- .../frontend-src/locales/ru.json | 67 +- .../frontend-src/locales/sv.json | 67 +- .../frontend-src/locales/tr.json | 67 +- .../frontend-src/locales/uk.json | 67 +- .../frontend-src/locales/zh.json | 67 +- .../frontend-src/maintenance-calendar-card.ts | 67 +- .../frontend-src/maintenance-card-editor.ts | 6 +- .../frontend-src/maintenance-card.ts | 53 +- .../maintenance-dashboard-strategy.ts | 31 +- .../frontend-src/maintenance-panel.ts | 161 +- .../frontend-src/maintenance-strategy-shim.ts | 4 +- .../frontend-src/package-lock.json | 8 + .../frontend-src/package.json | 2 + .../frontend-src/panel-styles.ts | 12 +- .../frontend-src/renderers/charts.ts | 11 +- .../frontend-src/renderers/history.ts | 16 +- .../frontend-src/renderers/progress.ts | 14 + .../frontend-src/renderers/sparkline.ts | 12 +- .../frontend-src/renderers/task-detail.ts | 12 + .../frontend-src/styles.ts | 14 +- .../frontend-src/tsconfig.test.json | 9 + .../frontend-src/types.ts | 31 +- .../frontend/__init__.py | 5 +- .../frontend/locales/cs.json | 67 +- .../frontend/locales/da.json | 67 +- .../frontend/locales/de.json | 67 +- .../frontend/locales/en.json | 67 +- .../frontend/locales/es.json | 67 +- .../frontend/locales/fi.json | 67 +- .../frontend/locales/fr.json | 67 +- .../frontend/locales/hi.json | 67 +- .../frontend/locales/hu.json | 67 +- .../frontend/locales/it.json | 67 +- .../frontend/locales/ja.json | 67 +- .../frontend/locales/ko.json | 67 +- .../frontend/locales/nb.json | 67 +- .../frontend/locales/nl.json | 67 +- .../frontend/locales/pl.json | 67 +- .../frontend/locales/pt-br.json | 67 +- .../frontend/locales/pt.json | 67 +- .../frontend/locales/ru.json | 67 +- .../frontend/locales/sv.json | 67 +- .../frontend/locales/tr.json | 67 +- .../frontend/locales/uk.json | 67 +- .../frontend/locales/zh.json | 67 +- .../frontend/maintenance-calendar-card.js | 588 +-- .../frontend/maintenance-card.js | 865 +++-- .../frontend/maintenance-panel.js | 1703 ++++---- .../frontend/maintenance-strategy-shim.js | 4 +- .../adopt-problem-sensors-dialog-FJ4YGWMJ.js | 301 ++ .../panel-chunks/area-view-JP4ZKMDQ.js | 277 ++ .../panel-chunks/areas-view-Z6D3T47X.js | 54 + .../frontend/panel-chunks/chunk-232HSTEK.js | 1489 +++++++ .../frontend/panel-chunks/chunk-2KTY4IVI.js | 217 ++ .../frontend/panel-chunks/chunk-2REBRMPM.js | 234 ++ .../frontend/panel-chunks/chunk-2U4DNN5T.js | 2 + .../frontend/panel-chunks/chunk-3DX24CU6.js | 2 + .../frontend/panel-chunks/chunk-3VJOJBZ5.js | 2 + .../frontend/panel-chunks/chunk-4OFQFCKD.js | 2 + .../frontend/panel-chunks/chunk-4PTWLV7V.js | 2 + .../frontend/panel-chunks/chunk-5YKNSANW.js | 2 + .../frontend/panel-chunks/chunk-AU57O2BC.js | 2 + .../frontend/panel-chunks/chunk-B7AS4DOM.js | 79 + .../frontend/panel-chunks/chunk-BA7DXCPM.js | 484 +++ .../frontend/panel-chunks/chunk-BFBOHWBW.js | 2 + .../frontend/panel-chunks/chunk-BYGTDXRD.js | 2 + .../frontend/panel-chunks/chunk-I7NCFUZX.js | 2 + .../frontend/panel-chunks/chunk-MHBW3M6D.js | 2 + .../frontend/panel-chunks/chunk-MJTDRBP6.js | 2 + .../frontend/panel-chunks/chunk-N6F5RX6S.js | 1309 +++++++ .../frontend/panel-chunks/chunk-PWKMLGJR.js | 2 + .../frontend/panel-chunks/chunk-QLW72JHE.js | 88 + .../frontend/panel-chunks/chunk-R26ZOQ5V.js | 1011 +++++ .../frontend/panel-chunks/chunk-RJY4VEHX.js | 2 + .../frontend/panel-chunks/chunk-VZYRPSO6.js | 2 + .../frontend/panel-chunks/chunk-WJT4RRLS.js | 2 + .../panel-chunks/complete-dialog-PYGTDA7N.js | 2 + .../panel-chunks/dialog-mount-FGPZ3MEC.js | 2 + .../panel-chunks/object-dialog-W3BBT6BL.js | 2 + .../panel-chunks/qr-dialog-FROADDFS.js | 2 + .../panel-chunks/settings-view-2LFKI3RI.js | 1465 +++++++ .../suggested-setups-dialog-ME7PK73B.js | 184 + .../panel-chunks/task-dialog-VVSVAG5I.js | 2 + .../chunks/budget-section-card-3BHOHMGQ.js | 127 + .../strategy/chunks/chunk-CTWCZMNP.js | 66 + .../strategy/chunks/chunk-EAYT244O.js | 2 + .../strategy/chunks/chunk-LKMEBXIJ.js | 60 + .../strategy/chunks/chunk-RGIYRWN4.js | 2 + .../strategy/chunks/chunk-SUKVNZGX.js | 88 + .../strategy/chunks/chunk-V2H6N4AC.js | 1309 +++++++ .../strategy/chunks/chunk-XOYZSKDM.js | 2 + .../strategy/chunks/dialog-mount-ZNHSIDHE.js | 3421 +++++++++++++++++ .../chunks/groups-section-card-N72HOEE7.js | 137 + .../chunks/vacation-section-card-KBV2JPSC.js | 134 + .../maintenance-dashboard-strategy.js | 6 +- .../helpers/battery_fleet.py | 30 + .../maintenance_supporter/helpers/dates.py | 32 + .../helpers/doc_archive.py | 377 +- .../helpers/documents.py | 21 +- .../helpers/entity_rename.py | 2 +- .../helpers/notification_manager.py | 53 +- .../maintenance_supporter/helpers/parts.py | 53 +- .../maintenance_supporter/helpers/pause.py | 36 +- .../helpers/problem_sensors.py | 10 +- .../helpers/qrcodegen.py | 18 +- .../maintenance_supporter/helpers/sanitize.py | 13 +- .../helpers/sensor_predictor.py | 6 +- .../helpers/settings_registry.py | 5 +- .../helpers/signatures/_discovery.py | 4 +- .../helpers/signatures/_model.py | 32 +- .../helpers/signatures/heating.py | 17 +- .../helpers/signatures/kitchen.py | 31 + .../helpers/signatures/printers.py | 3 +- .../helpers/trigger_fallback.py | 33 +- .../maintenance_supporter/intent.py | 364 +- .../maintenance_supporter/manifest.json | 2 +- .../models/maintenance_task.py | 23 +- .../maintenance_supporter/panel.py | 9 +- .../maintenance_supporter/parts_runtime.py | 23 +- .../maintenance_supporter/quality_scale.yaml | 11 +- .../maintenance_supporter/repairs.py | 57 +- .../maintenance_supporter/sensor.py | 18 +- .../maintenance_supporter/services.yaml | 17 +- .../maintenance_supporter/storage.py | 29 +- .../maintenance_supporter/strings.json | 105 +- .../maintenance_supporter/templates.py | 2 +- .../translations/cs.json | 105 +- .../translations/da.json | 105 +- .../translations/de.json | 105 +- .../translations/en.json | 105 +- .../translations/es.json | 105 +- .../translations/fi.json | 105 +- .../translations/fr.json | 105 +- .../translations/hi.json | 105 +- .../translations/hu.json | 105 +- .../translations/it.json | 105 +- .../translations/ja.json | 105 +- .../translations/ko.json | 105 +- .../translations/nb.json | 105 +- .../translations/nl.json | 105 +- .../translations/pl.json | 105 +- .../translations/pt-BR.json | 105 +- .../translations/pt.json | 105 +- .../translations/ru.json | 105 +- .../translations/sv.json | 105 +- .../translations/tr.json | 105 +- .../translations/uk.json | 105 +- .../translations/zh-Hans.json | 105 +- .../maintenance_supporter/trigger.py | 2 +- .../maintenance_supporter/views.py | 2 +- .../websocket/__init__.py | 11 +- .../websocket/dashboard.py | 10 +- .../maintenance_supporter/websocket/io.py | 955 +++-- .../websocket/objects.py | 43 +- .../maintenance_supporter/websocket/parts.py | 7 +- .../maintenance_supporter/websocket/tasks.py | 4 + .../websocket/tasks_actions.py | 13 +- .../websocket/tasks_history.py | 4 + .../websocket/tasks_lifecycle.py | 106 + .../websocket/tasks_persist.py | 88 +- .../websocket/tasks_validation.py | 4 + .../versatile_thermostat/base_thermostat.py | 56 +- .../versatile_thermostat/manifest.json | 4 +- .../opening_degree_algorithm.py | 2 + .../versatile_thermostat/prop_handler_tpi.py | 22 +- .../thermostat_climate_valve.py | 11 +- .../versatile_thermostat/thermostat_valve.py | 3 +- .../versatile_thermostat/translations/de.json | 4 +- .../versatile_thermostat/underlyings.py | 44 +- 258 files changed, 24621 insertions(+), 3136 deletions(-) create mode 100644 custom_components/maintenance_supporter/frontend-src/tsconfig.test.json create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/adopt-problem-sensors-dialog-FJ4YGWMJ.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/area-view-JP4ZKMDQ.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/areas-view-Z6D3T47X.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-232HSTEK.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-2KTY4IVI.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-2REBRMPM.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-2U4DNN5T.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-3DX24CU6.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-3VJOJBZ5.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-4OFQFCKD.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-4PTWLV7V.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-5YKNSANW.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-AU57O2BC.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-B7AS4DOM.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-BA7DXCPM.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-BFBOHWBW.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-BYGTDXRD.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-I7NCFUZX.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-MHBW3M6D.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-MJTDRBP6.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-N6F5RX6S.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-PWKMLGJR.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-QLW72JHE.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-R26ZOQ5V.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-RJY4VEHX.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VZYRPSO6.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-WJT4RRLS.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/complete-dialog-PYGTDA7N.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/dialog-mount-FGPZ3MEC.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/object-dialog-W3BBT6BL.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/qr-dialog-FROADDFS.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/settings-view-2LFKI3RI.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/suggested-setups-dialog-ME7PK73B.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/task-dialog-VVSVAG5I.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/budget-section-card-3BHOHMGQ.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-CTWCZMNP.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-EAYT244O.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-LKMEBXIJ.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-RGIYRWN4.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-SUKVNZGX.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-V2H6N4AC.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-XOYZSKDM.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/dialog-mount-ZNHSIDHE.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/groups-section-card-N72HOEE7.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/vacation-section-card-KBV2JPSC.js diff --git a/.ha_run.lock b/.ha_run.lock index eeb93823..8279ca34 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 70, "version": 1, "ha_version": "2026.9.4", "start_ts": 1790832768.8204901} \ No newline at end of file +{"pid": 72, "version": 1, "ha_version": "2026.9.4", "start_ts": 1790957177.2689593} \ No newline at end of file diff --git a/custom_components/maintenance_supporter/__init__.py b/custom_components/maintenance_supporter/__init__.py index ba72eca8..bd9e0504 100644 --- a/custom_components/maintenance_supporter/__init__.py +++ b/custom_components/maintenance_supporter/__init__.py @@ -47,7 +47,6 @@ from .const import ( CONF_ADVANCED_CHECKLISTS, CONF_ADVANCED_ENVIRONMENTAL, CONF_ADVANCED_GROUPS, - CONF_ADVANCED_PREDICTIONS, CONF_ADVANCED_SCHEDULE_TIME, CONF_ADVANCED_SEASONAL, CONF_BUDGET_MONTHLY, @@ -63,6 +62,7 @@ from .const import ( DOCUMENT_TEXT_INDEX_KEY, DOMAIN, EVENT_UNSUBS_KEY, + FLAT_SCHEDULE_TYPES, GLOBAL_UNIQUE_ID, MAX_COST, MAX_DURATION_MINUTES, @@ -214,12 +214,17 @@ SERVICE_ADD_OBJECT_SCHEMA = vol.Schema( # just a third UI onto the same storage, so a value the WS API rejects must not # be reachable from an automation. `vol.Coerce` stays (unlike the WS schemas) # because YAML/templated service data arrives as strings. +# Only the recurrences the flat service fields can express. "sensor_based" +# was offered but a service cannot set the trigger, so it silently became an +# interval or manual task (audit 2026-09-29); calendar kinds go via `schedule`. +_SERVICE_SCHEDULE_TYPE = vol.In(sorted(str(kind) for kind in FLAT_SCHEDULE_TYPES)) + SERVICE_ADD_TASK_SCHEMA = vol.Schema( { vol.Required("entry_id"): cv.string, vol.Required("name"): vol.All(cv.string, vol.Length(min=1, max=MAX_NAME_LENGTH)), vol.Optional("task_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)), - vol.Optional("schedule_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)), + vol.Optional("schedule_type"): _SERVICE_SCHEDULE_TYPE, vol.Optional("interval_days"): vol.All( vol.Coerce(int), vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1]) ), @@ -240,7 +245,7 @@ SERVICE_UPDATE_TASK_SCHEMA = vol.Schema( vol.Required("task_id"): cv.string, vol.Optional("name"): vol.All(cv.string, vol.Length(min=1, max=MAX_NAME_LENGTH)), vol.Optional("task_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)), - vol.Optional("schedule_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)), + vol.Optional("schedule_type"): _SERVICE_SCHEDULE_TYPE, vol.Optional("interval_days"): vol.All( vol.Coerce(int), vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1]) ), @@ -269,7 +274,7 @@ SERVICE_DELETE_TASK_SCHEMA = vol.Schema( SERVICE_LIST_TASKS_SCHEMA = vol.Schema( { vol.Optional("entry_id"): cv.string, - vol.Optional("status"): vol.In(["ok", "due_soon", "overdue", "triggered"]), + vol.Optional("status"): vol.In(["ok", "due_soon", "overdue", "triggered", "paused"]), } ) @@ -430,6 +435,8 @@ def _trigger_target_and_unit(hass: HomeAssistant, tc: dict[str, Any]) -> tuple[f return tc.get("trigger_runtime_hours"), "h" if ttype == TriggerType.STATE_CHANGE: return tc.get("trigger_target_changes"), None + if ttype == TriggerType.DUE_DATE: + return tc.get("trigger_days_before") or 0, "d" if ttype == TriggerType.COMPOUND: return None, None from .entity.triggers import primary_entity_id @@ -700,7 +707,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: include_history = call.data.get("include_history", True) # Phase 1: gather data on the event loop (accesses HA APIs) - data = build_export_data(hass, include_history=include_history) + from .helpers.import_mapping import async_attach_move_hints + + data = await async_attach_move_hints(hass, build_export_data(hass, include_history=include_history)) # Phase 2: serialize in executor (CPU-bound, no HA API calls) file_path = hass.config.path(f"maintenance_export.{fmt}") @@ -751,7 +760,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: entry_id=call.data["entry_id"], name=call.data["name"], task_type=call.data.get("task_type", "custom"), - schedule_type=call.data.get("schedule_type", "time_based"), + schedule_type=call.data.get("schedule_type"), interval_days=call.data.get("interval_days"), interval_unit=call.data.get("interval_unit", "days"), due_date=call.data.get("due_date"), @@ -1082,13 +1091,11 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: """ found: list[ConfigEntry] = [] seen: set[str] = set() - # 2026.8 gives a device exactly one config entry (`config_entry_id`); - # the plural `config_entries` is a silently-deprecated compat shim - # there — no report_usage, so the deprecation gate cannot see it — and - # the only spelling 2026.7 has. Prefer the singular where it exists. - single = getattr(device, "config_entry_id", None) - owner_ids = (single,) if single is not None else tuple(getattr(device, "config_entries", ()) or ()) - for ce_id in owner_ids: + # One config entry per device since 2026.8 (the plural is reported + # from 2026.10) — helpers.device_link.device_owner_ids reads it. + from .helpers.device_link import device_owner_ids + + for ce_id in device_owner_ids(device): ce = hass.config_entries.async_get_entry(ce_id) if ce is not None and ce.domain == DOMAIN and ce.unique_id != GLOBAL_UNIQUE_ID: found.append(ce) @@ -1281,7 +1288,6 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: def _detect_advanced_feature_usage(hass: HomeAssistant, global_options: dict[str, Any]) -> dict[str, bool]: """Scan existing entries to detect which advanced features are in use.""" adaptive = False - predictions = False seasonal = False environmental = False checklists = False @@ -1295,8 +1301,6 @@ def _detect_advanced_feature_usage(hass: HomeAssistant, global_options: dict[str ac = task_data.get("adaptive_config") or {} if ac.get("enabled"): adaptive = True - if ac.get("sensor_prediction_enabled"): - predictions = True if ac.get("seasonal_enabled"): seasonal = True if ac.get("environmental_entity"): @@ -1311,7 +1315,6 @@ def _detect_advanced_feature_usage(hass: HomeAssistant, global_options: dict[str return { CONF_ADVANCED_ADAPTIVE: adaptive, - CONF_ADVANCED_PREDICTIONS: predictions, CONF_ADVANCED_SEASONAL: seasonal, CONF_ADVANCED_ENVIRONMENTAL: environmental, CONF_ADVANCED_BUDGET: budget, @@ -1527,6 +1530,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf # HA-started so the store has finished loading. entry.async_on_unload(async_at_started(hass, _check_document_storage_issues)) + # Devices an object created in a mistaken area before 2.96 move back, + # and the empty leftover areas are offered for removal (repair issue). + from .helpers.area_leftovers import async_check_leftover_areas + + entry.async_on_unload(async_at_started(hass, async_check_leftover_areas)) + # Backfill the document search index (#171) a couple of minutes after # start — pypdf is CPU work, and boot is the wrong moment for it. entry.async_on_unload(async_at_started(hass, _schedule_document_text_backfill)) @@ -1996,9 +2005,14 @@ async def _check_task_responsible_user_orphans( changed = False for tid, td in tasks.items(): ruid = td.get("responsible_user_id") - pool = [u for u in (td.get("assignee_pool") or []) if isinstance(u, str)] + raw_pool = td.get("assignee_pool") + pool = [u for u in raw_pool if isinstance(u, str)] if isinstance(raw_pool, list) else [] pruned_pool = [u for u in pool if u in valid_ids] - resp_orphaned = bool(ruid) and ruid not in valid_ids + # A value that is no user id at all (a list from a malformed + # backup) is as orphaned as a deleted user's — `in valid_ids` + # raised on it and failed the global entry's setup on every start + # (bug audit 2026-09-29). + resp_orphaned = bool(ruid) and (not isinstance(ruid, str) or ruid not in valid_ids) if not resp_orphaned and pruned_pool == pool: new_tasks[tid] = td continue diff --git a/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml index b94adc47..0cbde5e2 100644 --- a/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml +++ b/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml @@ -2,25 +2,61 @@ # # Handkopieren ist nicht nötig: Einstellungen -> Allgemein -> "Assist-Sätze # installieren" einschalten, dann schreibt die Integration diese Datei nach -# config/custom_sentences/de/maintenance_supporter.yaml und lädt den -# Konversations-Agenten neu. Nur aus diesem Verzeichnis liest der klassische -# (Nicht-LLM-)Agent Sätze. LLM-basierte Assist-Pipelines brauchen davon -# nichts — dort sind die Intents automatisch als Tools verfügbar. +# config/custom_sentences/de/maintenance_supporter.yaml (und eine Kopie ohne ß +# nach de-CH/) und lädt den Konversations-Agenten neu. Nur aus diesem +# Verzeichnis liest der klassische (Nicht-LLM-)Agent Sätze. LLM-basierte +# Assist-Pipelines brauchen davon nichts — dort sind die Intents automatisch +# als Tools verfügbar. # # Eine eigene Bearbeitung ist ausdrücklich vorgesehen: sobald der Inhalt nicht # mehr dem entspricht, was wir geschrieben haben, fassen Aktualisierungen die # Datei nicht mehr an (und das Abschalten der Einstellung ebenso wenig). +# +# Eigene Sätze GEWINNEN gegen Home Assistants eingebaute, sobald beide passen. +# Ein Satz aus einem Verb, das die eingebauten auch benutzen (überspringen, +# stummschalten, pausieren), und einem freien Namen würde "überspringe dieses +# Lied" oder "Radio stummschalten" schlucken — solche Sätze tragen deshalb ein +# Wartungs-Wort (Wartung, Aufgabe, Erinnerung, "diesmal"). Ein Test lässt +# jeden eingebauten Satz gegen diese Datei laufen. language: "de" intents: MaintenanceSupporterListTasks: data: - sentences: - - "welche Wartung[en] (ist|sind) [gerade] (fällig|überfällig)" - - "was ist [an Wartung] (fällig|überfällig)" + - "welche Wartung[en] (ist|sind) [gerade] fällig" + - "was ist [an Wartung] fällig" - "steht [eine] Wartung an" - "was muss gewartet werden" - # Bezug: die fragende Person. HA übergibt dem Handler, wer gesprochen - # hat — damit antwortet dies mit deren Aufgaben (inkl. Rotationsdienst). + - "welche Aufgaben sind (fällig|offen)" + - sentences: + - "was ist überfällig" + - "welche (Wartung[en]|Aufgaben) (ist|sind) überfällig" + - "(ist|gibt es) (etwas|was) Überfälliges" + slots: + status: "overdue" + - sentences: + - "was ist heute fällig" + - "welche (Wartung[en]|Aufgaben) (ist|sind) heute fällig" + slots: + within_days: 0 + - sentences: + - "was ist morgen fällig" + slots: + within_days: 1 + - sentences: + - "was ist diese Woche fällig" + - "welche (Wartung[en]|Aufgaben) (ist|sind) diese Woche fällig" + - "was steht diese Woche an" + slots: + within_days: 7 + - sentences: + - "was ist diesen Monat fällig" + slots: + within_days: 30 + - sentences: + - "was ist (im|in der|in) {area} fällig" + - "welche (Wartung[en]|Aufgaben) (ist|sind) (im|in der|in) {area} fällig" + - "was muss (im|in der|in) {area} gewartet werden" - sentences: - "was muss ich (machen|erledigen|tun)" - "was sind meine (Aufgaben|Pflichten|Wartungen)" @@ -28,8 +64,6 @@ intents: - "bin ich [gerade] (dran|zuständig)" slots: scope: "mine" - # Bezug: der Raum, aus dem gefragt wurde — über den Bereich des - # Satelliten aufgelöst, damit ein Wandtablet für seinen Raum antwortet. - sentences: - "was ist hier [drin] (fällig|zu tun)" - "welche Wartung ist (hier|in diesem Raum) fällig" @@ -39,57 +73,121 @@ intents: MaintenanceSupporterCompleteTask: data: - sentences: - - "[die] Wartung[saufgabe] {task_name:name} (erledigen|abschließen)" - - "markiere [die] [Wartung[saufgabe]] {task_name:name} als erledigt" - - "ich habe [die] {task_name:name} (erledigt|gemacht|abgeschlossen)" + - "[die] (Wartung[saufgabe]|Aufgabe) {task_name:name} (erledigen|abschließen)" + - "markiere [Wartung[saufgabe]|Aufgabe] {task_name:name} als erledigt" + - "ich habe {task_name:name} (erledigt|gemacht|abgeschlossen)" + - "erledige {task_name:name}" + - " {task_name:name} (ist|wurde) erledigt" + - " {task_name:name} erledigt" MaintenanceSupporterTaskInstructions: data: - sentences: - - "wie (mache|erledige) ich [die] {task_name:name}" - - "(Anleitung|Anweisungen) für [die] {task_name:name}" - - "was brauche ich für [die] {task_name:name}" + - "wie (mache|erledige) ich {task_name:name}" + - "(Anleitung|Anweisungen) (für|zu|zum|zur) {task_name:name}" + - "was brauche ich für {task_name:name}" MaintenanceSupporterTaskDue: data: - sentences: - - "wann ist [die] {task_name:name} fällig" - - "wann muss ich [die] {task_name:name} (machen|erledigen)" + - "wann ist {task_name:name} fällig" + - "wann muss ich {task_name:name} (machen|erledigen)" + - "wann steht {task_name:name} an" MaintenanceSupporterSnoozeTask: data: - sentences: - - "[die] {task_name:name} (snoozen|stummschalten)" - - "Erinnerungen für [die] {task_name:name} (stummschalten|pausieren)" + - " Erinnerung[en] (für|zu|zum|zur|an) {task_name:name} (stummschalten|pausieren|schlummern)" + - "(schalte|stell|stelle) Erinnerung[en] (für|zu|zum|zur|an) {task_name:name} stumm" + - " (Wartung[saufgabe]|Aufgabe) {task_name:name} (snoozen|stummschalten|pausieren)" MaintenanceSupporterPartStock: data: - sentences: - "wie viele {part_name:name} haben wir [noch]" - - "wie ist der Bestand (von|an) {part_name:name}" + - "wie viele {part_name:name} sind [noch] (da|auf Lager|vorrätig)" + - "wie (ist|hoch ist) der (Bestand|Vorrat|Lagerbestand) (von|an|bei) {part_name:name}" MaintenanceSupporterPostponeTask: data: - sentences: - - "verschiebe [die|das|den] {part_name:name} um {days} Tag[e]" - - "[die|das|den] {part_name:name} um {days} Tag[e] verschieben" - - "schiebe [die|das|den] {part_name:name} um {days} Tag[e] nach hinten" + - "verschiebe {task_name:name} um {days} Tag[e]" + - " {task_name:name} um {days} Tag[e] verschieben" + - "schiebe {task_name:name} um {days} Tag[e] (nach hinten|auf)" - sentences: - - "verschiebe [die|das|den] {part_name:name} um eine Woche" - - "[die|das|den] {part_name:name} um eine Woche verschieben" + - "verschiebe {task_name:name} um eine Woche" + - " {task_name:name} um eine Woche verschieben" slots: days: 7 MaintenanceSupporterSkipTask: data: - sentences: - - "überspringe [die|das|den] {part_name:name} [diesmal]" - - "[die|das|den] {part_name:name} [diesmal] überspringen" + - "überspringe {task_name:name} (diesmal|dieses Mal)" + - " {task_name:name} (diesmal|dieses Mal) überspringen" + - "überspringe (Wartung[saufgabe]|Aufgabe) {task_name:name} [diesmal|dieses Mal]" + - " (Wartung[saufgabe]|Aufgabe) {task_name:name} [diesmal|dieses Mal] überspringen" + - "lass {task_name:name} (diesmal|dieses Mal) aus" + MaintenanceSupporterRecordReading: + data: + - sentences: + - "trage [den] (Zählerstand|Messwert|Wert|Stand) {reading_value:value} (für|bei|beim|zum|zur) {task_name:name} ein" + - "(erfasse|notiere) [den] (Zählerstand|Messwert|Wert|Stand) {reading_value:value} (für|bei|beim|zum|zur) {task_name:name}" + - "der (Zählerstand|Messwert|Stand) (von|vom|für|beim) {task_name:name} (ist|beträgt) {reading_value:value}" + - " {task_name:name} (steht auf|zeigt) {reading_value:value}" + MaintenanceSupporterAddNote: + data: + - sentences: + - "notiere (für|zu|zum|zur|bei|beim) {task_name:name} dass {note_text:note}" + - "(schreibe|mache) [mir] eine Notiz (für|zu|zum|zur|bei|beim) {task_name:name} dass {note_text:note}" + - "Notiz (für|zu|zum|zur|bei|beim) {task_name:name} mit dem Text {note_text:note}" + MaintenanceSupporterWhoseTurn: + data: + - sentences: + - "wer ist (mit|bei|beim|für) {task_name:name} dran" + - "wer ist für {task_name:name} zuständig" + - "wer muss {task_name:name} (machen|erledigen)" + - "wer ist (mit|bei|beim) {task_name:name} an der Reihe" + MaintenanceSupporterShoppingList: + data: + - sentences: + - "was (müssen|sollten) wir (kaufen|nachkaufen|besorgen)" + - "was muss (gekauft|nachgekauft|nachbestellt|besorgt) werden" + - "welche (Ersatzteile|Teile|Verbrauchsmaterialien|Vorräte) (sind knapp|gehen zur Neige|müssen nachgekauft werden|brauchen wir)" + - "was (ist|wird) [gerade] knapp" + MaintenanceSupporterBoughtPart: + data: + - sentences: + - "(ich habe|wir haben) {quantity} [neue] {part_name:name} gekauft" + - "(ich habe|wir haben) (neue|neuen|neues) {part_name:name} gekauft" + - "buche {quantity} {part_name:name} (ein|in den Bestand)" + MaintenanceSupporterLowBatteries: + data: + - sentences: + - "welche Batterien sind (leer|schwach|fast leer|bald leer)" + - "(sind|ist) (irgendwelche|irgendeine|eine) Batterie[n] (leer|schwach)" + - "welche Batterien (muss ich|müssen wir|sollte ich) (wechseln|tauschen)" + MaintenanceSupporterUndo: + data: + - sentences: + - "(mach|mache) das rückgängig" + - "(mach|mache) die letzte (Aktion|Änderung|Wartungsaktion) rückgängig" + - "das war (ein Fehler|falsch)" lists: - # NOT called 'name': that is Home Assistant's built-in list of exposed - # ENTITY names, and the default agent resolves it against the registry - # before the handler runs — every sentence using it answered 'I am not - # aware of any device called ...' instead of reaching us. The - # {list:slot} alias keeps the value arriving in the 'name' slot. + # Nicht 'name' — das ist Home Assistants eingebaute Liste der ENTITÄTS- + # namen (siehe die englische Datei). {area} ist Home Assistants eigene + # Bereichsliste. task_name: wildcard: true part_name: wildcard: true + reading_value: + wildcard: true + note_text: + wildcard: true days: range: from: 1 to: 365 + quantity: + range: + from: 1 + to: 100 +expansion_rules: + # Eigener Präfix: diese Regeln werden mit Home Assistants eingebauten + # zusammengeführt, ein gleichnamiger Eintrag würde deren Regel ersetzen. + ms_art: "[der|die|das|den|dem]" diff --git a/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml index e85295c6..ba9d4c08 100644 --- a/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml +++ b/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml @@ -10,15 +10,50 @@ # Editing your own copy is fine and expected: once its content no longer # matches what we wrote, upgrades leave it alone (and so does turning the # setting back off). +# +# Custom sentences WIN over Home Assistant's built-in ones whenever both +# match. A sentence that starts with a verb the built-ins also use (skip, +# pause, mute, complete) followed by a free-text name would swallow "skip +# this song" or "pause the radio" — so those carry a maintenance word +# (maintenance, task, chore, reminder, "this time"). A test runs every +# built-in sentence against this file to keep it that way. language: "en" intents: MaintenanceSupporterListTasks: data: - sentences: - - "what maintenance is (due|overdue)" - - "which maintenance [tasks] (are|is) (due|overdue|pending)" + - "what maintenance is (due|pending)" + - "which maintenance [tasks] (are|is) (due|pending)" - "is [there] any maintenance due" - "what needs (maintenance|servicing)" + - "what (chores|maintenance tasks) are (due|pending)" + - sentences: + - "what [maintenance] is overdue" + - "which [maintenance] (tasks|chores) are overdue" + - "is (anything|any maintenance) overdue" + slots: + status: "overdue" + - sentences: + - "what [maintenance] is due today" + - "what (chores|maintenance tasks) are due today" + slots: + within_days: 0 + - sentences: + - "what [maintenance] is due tomorrow" + slots: + within_days: 1 + - sentences: + - "what [maintenance] is due this week" + - "what (chores|maintenance tasks) are due this week" + slots: + within_days: 7 + - sentences: + - "what [maintenance] is due this month" + slots: + within_days: 30 + - sentences: + - "what [maintenance] is due in [the] {area}" + - "what needs (maintenance|servicing) in [the] {area}" # Scope: the person asking. HA tells the handler who spoke, so this # answers with that user's tasks (and their turn on a rotating chore). - sentences: @@ -39,57 +74,121 @@ intents: MaintenanceSupporterCompleteTask: data: - sentences: - - "complete [the] [maintenance] [task] {task_name:name}" - - "mark [the] [maintenance] [task] {task_name:name} [as] (done|completed)" - - "I (did|finished) [the] {task_name:name}" + - "complete [the] (maintenance [task]|task|chore) [for] [the] {task_name:name}" + - "complete [the] {task_name:name} (maintenance|task|chore)" + - "mark [the] [maintenance] [task|chore] {task_name:name} [as] (done|completed|complete)" + - "I('ve| have) (done|finished|completed) [the] {task_name:name}" + - "I (did|finished|completed) [the] {task_name:name}" + - "[the] {task_name:name} is (done|finished|completed)" MaintenanceSupporterTaskInstructions: data: - sentences: - "how do I (do|perform) [the] {task_name:name}" - - "(instructions|guidance) for [the] {task_name:name}" + - "(instructions|guidance|steps) for [the] {task_name:name}" - "what do I need for [the] {task_name:name}" MaintenanceSupporterTaskDue: data: - sentences: - "when is [the] {task_name:name} due" - "when do I (need|have) to (do|perform) [the] {task_name:name}" + - "when is [the] next {task_name:name}" MaintenanceSupporterSnoozeTask: data: - sentences: - - "snooze [the] [maintenance] [task] {task_name:name}" - - "(mute|silence) [the] reminders for [the] {task_name:name}" + - "snooze [the] (reminder|reminders|notification|notifications) (for|about|on) [the] {task_name:name}" + - "snooze [the] {task_name:name} (reminder|reminders|notification|notifications|maintenance|task|chore)" + - "snooze [the] (maintenance [task]|task|chore) {task_name:name}" + - "(mute|silence|pause) [the] [maintenance] (reminder|reminders|notification|notifications) (for|about|on) [the] {task_name:name}" MaintenanceSupporterPartStock: data: - sentences: - - "how (many|much) {part_name:name} (do we have|are left|is left)" + - "how (many|much) {part_name:name} (do we have [left]|are left|is left|have we got)" + - "how many {part_name:name} are in stock" - "what is the stock of [the] {part_name:name}" MaintenanceSupporterPostponeTask: data: - sentences: - - "postpone [the] {part_name:name} by {days} day[s]" - - "push [the] {part_name:name} back [by] {days} day[s]" - - "delay [the] {part_name:name} [by] {days} day[s]" + - "postpone [the] {task_name:name} (by|for) {days} day[s]" + - "push [the] {task_name:name} back [by] {days} day[s]" + - "delay [the] {task_name:name} [by] {days} day[s]" - sentences: - - "postpone [the] {part_name:name} by a week" - - "push [the] {part_name:name} back [by] a week" + - "(postpone|delay) [the] {task_name:name} (by|for) a week" + - "push [the] {task_name:name} back [by] a week" slots: days: 7 MaintenanceSupporterSkipTask: data: - sentences: - - "skip [the] {part_name:name} [this time]" - - "skip this (cycle|round) of [the] {part_name:name}" + - "skip [the] {task_name:name} this time" + - "skip this (cycle|round|time) of [the] {task_name:name}" + - "skip [the] (maintenance [task]|task|chore) {task_name:name} [this time]" + - "skip [the] {task_name:name} (maintenance|task|chore) [this time]" + MaintenanceSupporterRecordReading: + data: + - sentences: + - "record [a] reading [of] {reading_value:value} for [the] {task_name:name}" + - "(log|enter) [a] reading [of] {reading_value:value} for [the] {task_name:name}" + - "[the] {task_name:name} reading is {reading_value:value}" + - "[the] {task_name:name} reads {reading_value:value}" + MaintenanceSupporterAddNote: + data: + - sentences: + - "add [a] note (to|for|on) [the] {task_name:name} (saying|that says) {note_text:note}" + - "(make|take) a note (for|on) [the] {task_name:name} (saying|that) {note_text:note}" + MaintenanceSupporterWhoseTurn: + data: + - sentences: + - "whose turn is it (to do|for) [the] {task_name:name}" + - "who is (responsible|in charge) (for|of) [the] {task_name:name}" + - "who (has to|needs to|should) do [the] {task_name:name} [next]" + MaintenanceSupporterShoppingList: + data: + - sentences: + - "what (do|should) we (need to|have to) buy" + - "what do we need to (buy|restock|reorder)" + - "what (spare parts|parts|supplies|consumables) (do we need|are low|are running low|need restocking)" + - "what needs (restocking|reordering)" + - "what is running low" + MaintenanceSupporterBoughtPart: + data: + - sentences: + - "(I|we) (bought|purchased|restocked) {quantity} [new|more] {part_name:name}" + - "(I|we) (bought|purchased|restocked) [some] (new|more) {part_name:name}" + - "add {quantity} {part_name:name} to [the] (stock|inventory)" + MaintenanceSupporterLowBatteries: + data: + - sentences: + - "which batteries (are|is) (low|empty|running low|flat)" + - "(are|is) (there|any) batter(y|ies) (low|empty|running low|flat)" + - "are there any low batteries" + - "which batteries (do|should) (I|we) (replace|change)" + MaintenanceSupporterUndo: + data: + - sentences: + - "undo (that|this|it)" + - "undo the last [maintenance] (action|change|completion)" + - "that was a mistake" + - "revert (that|the last [maintenance] (action|change))" lists: # NOT called 'name': that is Home Assistant's built-in list of exposed # ENTITY names, and the default agent resolves it against the registry # before the handler runs — every sentence using it answered 'I am not # aware of any device called ...' instead of reaching us. The # {list:slot} alias keeps the value arriving in the 'name' slot. + # ({area} is Home Assistant's own list of area names.) task_name: wildcard: true part_name: wildcard: true + reading_value: + wildcard: true + note_text: + wildcard: true days: range: from: 1 to: 365 + quantity: + range: + from: 1 + to: 100 diff --git a/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml index 2c36aee4..12d22095 100644 --- a/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml +++ b/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml @@ -10,6 +10,13 @@ # Puedes editar tu copia: en cuanto su contenido deje de coincidir con lo que # escribimos, las actualizaciones ya no la tocan (y desactivar el ajuste # tampoco la borra). +# +# Las frases propias GANAN a las de Home Assistant cuando ambas encajan. Un +# verbo que las integradas también usan (saltar, pausar, silenciar) seguido de +# un nombre libre se tragaría «salta esta canción» o «silencia la radio»; por +# eso esas frases llevan una palabra de mantenimiento (mantenimiento, tarea, +# avisos, «esta vez»). Un test comprueba cada frase integrada contra este +# archivo. language: "es" intents: MaintenanceSupporterListTasks: @@ -19,8 +26,34 @@ intents: - "qué (tareas|mantenimientos) [de mantenimiento] (hay|están) pendientes" - "hay (algo|algún mantenimiento) pendiente" - "qué hay que (revisar|mantener)" - # Ámbito: quien pregunta. HA le indica al gestor quién ha hablado, así - # que esto responde con SUS tareas (incluido su turno en la rotación). + - sentences: + - "qué (mantenimiento|tarea) está (atrasado|atrasada|vencido|vencida)" + - "qué (mantenimientos|tareas) están (atrasados|atrasadas|vencidos|vencidas)" + - "qué está (atrasado|vencido)" + - "hay algo (atrasado|vencido)" + slots: + status: "overdue" + - sentences: + - "qué (toca|hay que hacer) hoy" + - "qué mantenimiento toca hoy" + slots: + within_days: 0 + - sentences: + - "qué (toca|hay que hacer) mañana" + slots: + within_days: 1 + - sentences: + - "qué (toca|hay que hacer) esta semana" + - "qué mantenimiento toca esta semana" + slots: + within_days: 7 + - sentences: + - "qué (toca|hay que hacer) este mes" + slots: + within_days: 30 + - sentences: + - "qué (toca|hay que hacer) en [el|la] {area}" + - "qué mantenimiento (toca|hay) en [el|la] {area}" - sentences: - "qué me toca [a mí] [hacer]" - "qué tengo que hacer [yo]" @@ -28,8 +61,6 @@ intents: - "tengo algo pendiente" slots: scope: "mine" - # Ámbito: la habitación desde la que se pregunta, deducida de la zona del - # satélite — así una tableta de pared responde por su propia habitación. - sentences: - "qué hay que hacer aquí" - "qué mantenimiento (toca|hay) (aquí|en esta habitación)" @@ -39,54 +70,106 @@ intents: MaintenanceSupporterCompleteTask: data: - sentences: - - "[ya] he (hecho|terminado|completado) [el|la|los|las] {task_name:name}" - - "marca [el|la|los|las] {task_name:name} como (hecho|hecha|terminado|terminada|completado|completada)" + - "[ya] he (hecho|terminado|completado) {task_name:name}" + - "marca (mantenimiento|tarea) [de|del|de la] {task_name:name} como (hecho|hecha|terminado|terminada|completado|completada)" + - " {task_name:name} (está|están) (hecho|hecha|hechos|hechas|terminado|terminada|listo|lista)" MaintenanceSupporterTaskInstructions: data: - sentences: - - "cómo (hago|se hace) [el|la|los|las] {task_name:name}" - - "(instrucciones|indicaciones) para [el|la|los|las] {task_name:name}" - - "qué necesito para [el|la|los|las] {task_name:name}" + - "cómo (hago|se hace) {task_name:name}" + - "(instrucciones|indicaciones) para {task_name:name}" + - "qué necesito para {task_name:name}" MaintenanceSupporterTaskDue: data: - sentences: - - "cuándo toca [el|la|los|las] {task_name:name}" - - "cuándo (hay que|tengo que) hacer [el|la|los|las] {task_name:name}" + - "cuándo toca {task_name:name}" + - "cuándo (hay que|tengo que) hacer {task_name:name}" + - "cuándo vence {task_name:name}" MaintenanceSupporterSnoozeTask: data: - sentences: - - "(silencia|pausa) [el|la|los|las] {task_name:name}" - - "(silencia|pausa) los avisos (de|del|de la) {task_name:name}" + - "(silencia|pausa|pospón) los (avisos|recordatorios) (de|del|de la|de los|de las) {task_name:name}" + - "(silencia|pausa) (el mantenimiento|la tarea) [de|del|de la] {task_name:name}" MaintenanceSupporterPartStock: data: - sentences: - - "cuántos {part_name:name} (nos quedan|tenemos|quedan)" + - "cuánt(os|as) {part_name:name} (nos quedan|tenemos)" - "cuánto stock (hay|tenemos) de {part_name:name}" MaintenanceSupporterPostponeTask: data: - sentences: - - "(aplaza|retrasa|pospón) [el|la|los|las] {task_name:name} {days} día[s]" - - "deja [el|la|los|las] {task_name:name} para dentro de {days} día[s]" + - "(aplaza|retrasa|pospón) {task_name:name} {days} día[s]" + - "deja {task_name:name} para dentro de {days} día[s]" - sentences: - - "(aplaza|retrasa|pospón) [el|la|los|las] {task_name:name} una semana" + - "(aplaza|retrasa|pospón) {task_name:name} una semana" slots: days: 7 MaintenanceSupporterSkipTask: data: - sentences: - - "(salta|sáltate) [el|la|los|las] {task_name:name} [esta vez]" + - "(salta|sáltate) {task_name:name} esta vez" + - "(salta|sáltate) (el mantenimiento|la tarea) [de|del|de la] {task_name:name} [esta vez]" - "(salta|sáltate) esta (ronda|vuelta) (de|del|de la|de los|de las) {task_name:name}" + MaintenanceSupporterRecordReading: + data: + - sentences: + - "(registra|anota|apunta) [una] lectura de {reading_value:value} (para|en) {task_name:name}" + - "la lectura (de|del|de la) {task_name:name} es [de] {reading_value:value}" + - " {task_name:name} marca {reading_value:value}" + MaintenanceSupporterAddNote: + data: + - sentences: + - "(añade|agrega|pon) una nota (a|al|a la|para|en) {task_name:name} (que diga|diciendo) [que] {note_text:note}" + - "anota (para|en) {task_name:name} que {note_text:note}" + MaintenanceSupporterWhoseTurn: + data: + - sentences: + - "a quién le toca [hacer] {task_name:name}" + - "quién (es responsable|se encarga) (de|del|de la|de los|de las) {task_name:name}" + - "quién tiene que hacer {task_name:name}" + MaintenanceSupporterShoppingList: + data: + - sentences: + - "qué (tenemos que|hay que|necesitamos) comprar" + - "qué (repuestos|piezas|consumibles|suministros) (faltan|necesitamos|hay que reponer)" + - "qué (falta|se está acabando|hay que reponer)" + MaintenanceSupporterBoughtPart: + data: + - sentences: + - "(he comprado|compré|hemos comprado|compramos) {quantity} {part_name:name}" + - "(he comprado|compré|hemos comprado|compramos) más {part_name:name}" + - "añade {quantity} {part_name:name} al (stock|inventario)" + MaintenanceSupporterLowBatteries: + data: + - sentences: + - "qué (pilas|baterías) (están|tienen) (bajas|agotadas|poca carga|casi agotadas)" + - "hay (pilas|baterías) (bajas|agotadas)" + - "qué (pilas|baterías) (tengo|hay) que (cambiar|reemplazar)" + MaintenanceSupporterUndo: + data: + - sentences: + - "deshaz (eso|lo último|la última acción)" + - "deshacer (eso|la última acción)" + - "eso fue un error" lists: - # NO uses «name»: es la lista integrada de nombres de ENTIDADES de Home - # Assistant, que el agente por defecto resuelve antes de llamar al gestor — - # cualquier frase que la usara respondía «no conozco ningún dispositivo…» en - # vez de llegar hasta nosotros. El alias {lista:slot} mantiene el valor en el - # slot «name». + # No 'name': es la lista integrada de nombres de ENTIDADES de Home Assistant + # (ver el archivo inglés). {area} es la lista de áreas de Home Assistant. task_name: wildcard: true part_name: wildcard: true + reading_value: + wildcard: true + note_text: + wildcard: true days: range: from: 1 to: 365 + quantity: + range: + from: 1 + to: 100 +expansion_rules: + # Prefijo propio: estas reglas se fusionan con las de Home Assistant. + ms_el: "[el|la|los|las]" diff --git a/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml index e50188bf..863c12d1 100644 --- a/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml +++ b/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml @@ -10,24 +10,54 @@ # Vous pouvez modifier votre copie : dès que son contenu ne correspond plus à # ce que nous avons écrit, les mises à jour n'y touchent plus (et désactiver le # réglage ne la supprime pas non plus). +# +# Les phrases personnalisées l'EMPORTENT sur celles de Home Assistant dès que +# les deux correspondent. Un verbe que les phrases intégrées utilisent aussi +# (sauter, passer, mettre en pause) suivi d'un nom libre avalerait « passe à +# la chanson suivante » — ces phrases portent donc un mot d'entretien +# (entretien, tâche, rappels, « cette fois »). Un test vérifie chaque phrase +# intégrée contre ce fichier. language: "fr" intents: MaintenanceSupporterListTasks: data: - sentences: - - "quel[le]s (entretien|entretiens|maintenances|tâches) (est|sont) (dû|due|dues|prévus|prévues)" + - "quel[le]s (entretien|entretiens|maintenances|tâches) (est|sont) (dû|due|dues|prévus|prévues|à faire)" - "qu'est-ce qui est à (faire|entretenir)" - "y a-t-il [de] l'entretien à faire" - # Portée : la personne qui parle. HA transmet au gestionnaire qui a parlé, - # donc ceci répond avec SES tâches (y compris son tour de rotation). + - sentences: + - "qu'est-ce qui est en retard" + - "quel[le]s (entretiens|tâches|maintenances) (est|sont) en retard" + - "y a-t-il (quelque chose|des tâches) en retard" + slots: + status: "overdue" + - sentences: + - "qu'est-ce qui est à faire aujourd'hui" + - "quel[le]s (entretiens|tâches) (sont|est) (prévu|prévue|prévus|prévues) aujourd'hui" + slots: + within_days: 0 + - sentences: + - "qu'est-ce qui est à faire demain" + slots: + within_days: 1 + - sentences: + - "qu'est-ce qui est à faire cette semaine" + - "quel[le]s (entretiens|tâches) (sont|est) (prévu|prévue|prévus|prévues) cette semaine" + slots: + within_days: 7 + - sentences: + - "qu'est-ce qui est à faire ce mois-ci" + slots: + within_days: 30 + - sentences: + - "qu'est-ce qui est à faire (dans|au|à) [la |le |l']{area}" + - "quel entretien est (dû|prévu) (dans|au|à) [la |le |l']{area}" - sentences: - "qu'est-ce que je dois faire" - "quelles sont mes tâches" - "c'est à moi de faire quelque chose" slots: scope: "mine" - # Portée : la pièce d'où vient la demande, résolue via la zone du - # satellite — une tablette murale répond ainsi pour sa propre pièce. - sentences: - "qu'est-ce qu'il y a à faire ici" - "quel entretien est (dû|prévu) (ici|dans cette pièce)" @@ -36,50 +66,109 @@ intents: MaintenanceSupporterCompleteTask: data: - sentences: - - "j'ai (fait|terminé|fini) [le|la|les|l'] {task_name:name}" - - "marque [le|la|les|l'] {task_name:name} comme (fait|faite|terminé|terminée)" + - "j'ai (fait|terminé|fini) {task_name:name}" + - "marque (entretien|tâche) {task_name:name} comme (fait|faite|terminé|terminée)" + - "{task_name:name} est (fait|faite|terminé|terminée)" MaintenanceSupporterTaskInstructions: data: - sentences: - - "comment (faire|je fais) [le|la|les|l'] {task_name:name}" - - "(instructions|consignes) pour [le|la|les|l'] {task_name:name}" + - "comment (faire|je fais) {task_name:name}" + - "(instructions|consignes) pour {task_name:name}" + - "de quoi ai-je besoin pour {task_name:name}" MaintenanceSupporterTaskDue: data: - sentences: - - "quand est-ce que [le|la|les|l'] {task_name:name} est (dû|due|prévu|prévue)" - - "quand faut-il faire [le|la|les|l'] {task_name:name}" + - "quand est-ce que {task_name:name} est (dû|due|prévu|prévue)" + - "quand faut-il faire {task_name:name}" + - "quand dois-je faire {task_name:name}" MaintenanceSupporterSnoozeTask: data: - sentences: - - "mets [le|la|les|l'] {task_name:name} en pause" + - "(mets|mettre) en pause les rappels (de|du|de la|des|pour) [l']{task_name:name}" + - "(coupe|désactive|suspends) les rappels (de|du|de la|des|pour) [l']{task_name:name}" + - "mets (l'entretien|la tâche) {task_name:name} en pause" MaintenanceSupporterPartStock: data: - sentences: - - "combien de {part_name:name} (reste-t-il|il reste|avons-nous)" + - "combien de {part_name:name} (reste-t-il|il reste|avons-nous|nous reste-t-il)" + - "quel est le stock (de|du|de la|des|d') {part_name:name}" MaintenanceSupporterPostponeTask: data: - sentences: - - "reporte [le|la|les|l'] {task_name:name} de {days} jour[s]" - - "repousse [le|la|les|l'] {task_name:name} de {days} jour[s]" + - "reporte {task_name:name} de {days} jour[s]" + - "repousse {task_name:name} de {days} jour[s]" - sentences: - - "reporte [le|la|les|l'] {task_name:name} d'une semaine" - - "repousse [le|la|les|l'] {task_name:name} d'une semaine" + - "reporte {task_name:name} d'une semaine" + - "repousse {task_name:name} d'une semaine" slots: days: 7 MaintenanceSupporterSkipTask: data: - sentences: - - "(saute|passe) [le|la|les|l'] {task_name:name} [cette fois]" + - "(saute|passe) {task_name:name} cette fois" + - "(saute|passe) (l'entretien|la tâche) {task_name:name} [cette fois]" + - "(saute|passe) ce (tour|cycle) (de|du|de la|des|d') {task_name:name}" + MaintenanceSupporterRecordReading: + data: + - sentences: + - "(enregistre|note|saisis) [un|le] relevé (de|à) {reading_value:value} pour {task_name:name}" + - "le relevé (de|du|de la|des|d') {task_name:name} est [de] {reading_value:value}" + - "{task_name:name} (affiche|indique) {reading_value:value}" + MaintenanceSupporterAddNote: + data: + - sentences: + - "(ajoute|mets) une note (à|au|à la|aux|pour|sur) [l']{task_name:name} (disant|qui dit) [que] {note_text:note}" + - "note pour {task_name:name} que {note_text:note}" + MaintenanceSupporterWhoseTurn: + data: + - sentences: + - "c'est à qui (de faire|pour) {task_name:name}" + - "à qui le tour pour {task_name:name}" + - "qui (doit faire|s'occupe de|est responsable de) {task_name:name}" + MaintenanceSupporterShoppingList: + data: + - sentences: + - "qu'est-ce qu'il faut (acheter|racheter)" + - "qu'est-ce qu'on doit (acheter|racheter)" + - "quelles (pièces|pièces de rechange|fournitures|consommables) (manquent|faut-il racheter)" + - "qu'est-ce qui (manque|est en rupture) [de stock]" + MaintenanceSupporterBoughtPart: + data: + - sentences: + - "(j'ai|nous avons) acheté {quantity} [nouveaux|nouvelles] {part_name:name}" + - "(j'ai|nous avons) acheté (des|du|de la|de nouveaux|de nouvelles) {part_name:name}" + - "ajoute {quantity} {part_name:name} au stock" + MaintenanceSupporterLowBatteries: + data: + - sentences: + - "quelles (piles|batteries) sont (faibles|vides|à plat|presque vides)" + - "(y a-t-il|est-ce qu'il y a) des (piles|batteries) (faibles|vides|à changer)" + - "quelles (piles|batteries) (faut-il|dois-je) (changer|remplacer)" + MaintenanceSupporterUndo: + data: + - sentences: + - "annule (ça|cela|la dernière action|la dernière modification)" + - "c'était une erreur" lists: - # PAS « name » : c'est la liste intégrée des noms d'ENTITÉS de Home Assistant, - # que l'agent par défaut résout avant d'appeler le gestionnaire — toute phrase - # l'utilisant répondait « je ne connais aucun appareil… » au lieu de nous - # atteindre. L'alias {liste:slot} garde la valeur dans le slot « name ». + # Pas 'name' : c'est la liste intégrée des noms d'ENTITÉS de Home Assistant + # (voir le fichier anglais). {area} est la liste des pièces de Home Assistant. task_name: wildcard: true part_name: wildcard: true + reading_value: + wildcard: true + note_text: + wildcard: true days: range: from: 1 to: 365 + quantity: + range: + from: 1 + to: 100 +expansion_rules: + # Préfixe propre : ces règles sont fusionnées avec celles de Home Assistant, + # une règle du même nom remplacerait la sienne (« le » y existe déjà). + ms_le: "[le |la |les |l']" diff --git a/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml index e5e4114d..a63d282f 100644 --- a/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml +++ b/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml @@ -10,6 +10,13 @@ # Modificare la propria copia è previsto: appena il contenuto non corrisponde # più a quello che abbiamo scritto, gli aggiornamenti non lo toccano più (e # nemmeno disattivare l'impostazione lo cancella). +# +# Le frasi personalizzate VINCONO su quelle di Home Assistant quando entrambe +# corrispondono. Un verbo usato anche da quelle integrate (saltare, mettere in +# pausa, silenziare) seguito da un nome libero ingoierebbe «salta questa +# canzone» o «metti in pausa il timer»: queste frasi portano quindi una parola +# di manutenzione (manutenzione, attività, promemoria, «per questa volta»). Un +# test controlla ogni frase integrata contro questo file. language: "it" intents: MaintenanceSupporterListTasks: @@ -20,8 +27,33 @@ intents: - "cosa c'è da fare" - "c'è [qualche] manutenzione da fare" - "cosa bisogna (fare|controllare)" - # Ambito: chi sta parlando. HA passa al gestore chi ha parlato, quindi - # questo risponde con le SUE attività (incluso il suo turno di rotazione). + - sentences: + - "cosa è in ritardo" + - "quali (manutenzioni|attività) sono (scadute|in ritardo)" + - "c'è qualcosa (di scaduto|in ritardo)" + slots: + status: "overdue" + - sentences: + - "cosa c'è da fare oggi" + - "quale manutenzione scade oggi" + slots: + within_days: 0 + - sentences: + - "cosa c'è da fare domani" + slots: + within_days: 1 + - sentences: + - "cosa c'è da fare questa settimana" + - "quali manutenzioni scadono questa settimana" + slots: + within_days: 7 + - sentences: + - "cosa c'è da fare questo mese" + slots: + within_days: 30 + - sentences: + - "cosa c'è da fare (in|nel|nella|nello|nell') {area}" + - "quale manutenzione serve (in|nel|nella|nello|nell') {area}" - sentences: - "cosa devo fare [io]" - "quali sono i miei (compiti|lavori|impegni)" @@ -29,8 +61,6 @@ intents: - "ho qualcosa da fare" slots: scope: "mine" - # Ambito: la stanza da cui arriva la richiesta, ricavata dall'area del - # satellite — così un tablet a muro risponde per la stanza in cui si trova. - sentences: - "cosa c'è da fare (qui|in questa stanza)" - "quale manutenzione serve (qui|in questa stanza)" @@ -40,53 +70,106 @@ intents: MaintenanceSupporterCompleteTask: data: - sentences: - - "ho (fatto|finito|completato) [il|lo|la|l'|i|gli|le] {task_name:name}" - - "segna [il|lo|la|l'|i|gli|le] {task_name:name} come (fatto|fatta|completato|completata)" + - "ho (fatto|finito|completato) {task_name:name}" + - "segna {task_name:name} come (fatto|fatta|completato|completata)" + - "{task_name:name} è (fatto|fatta|finito|finita)" + - "completa (la manutenzione|l'attività) {task_name:name}" MaintenanceSupporterTaskInstructions: data: - sentences: - - "come (faccio|si fa) [il|lo|la|l'|i|gli|le] {task_name:name}" - - "(istruzioni|indicazioni) per [il|lo|la|l'|i|gli|le] {task_name:name}" - - "cosa mi serve per [il|lo|la|l'|i|gli|le] {task_name:name}" + - "come (faccio|si fa) {task_name:name}" + - "(istruzioni|indicazioni) per {task_name:name}" + - "cosa mi serve per {task_name:name}" MaintenanceSupporterTaskDue: data: - sentences: - - "quando (scade|va fatto|va fatta) [il|lo|la|l'|i|gli|le] {task_name:name}" - - "quando devo fare [il|lo|la|l'|i|gli|le] {task_name:name}" + - "quando (scade|va fatto|va fatta) {task_name:name}" + - "quando devo fare {task_name:name}" MaintenanceSupporterSnoozeTask: data: - sentences: - - "(silenzia|metti in pausa) [il|lo|la|l'|i|gli|le] {task_name:name}" - - "(silenzia|sospendi) i promemoria (di|del|della|dei|delle|per) {task_name:name}" + - "(silenzia|sospendi|metti in pausa) i promemoria (di|del|della|dello|dell'|dei|delle|per) {task_name:name}" + - "(silenzia|sospendi) (la manutenzione|l'attività) {task_name:name}" MaintenanceSupporterPartStock: data: - sentences: - - "quanti {part_name:name} (abbiamo|ci sono|restano)" - - "quante scorte di {part_name:name} (abbiamo|ci sono)" + - "quant(i|e) {part_name:name} (abbiamo|restano|ci restano) [ancora]" + - "quante scorte (di|del|della|dei|delle) {part_name:name} (abbiamo|ci sono)" + - "quant(i|e) {part_name:name} ci sono in (magazzino|scorta)" MaintenanceSupporterPostponeTask: data: - sentences: - - "(rimanda|rinvia|posticipa) [il|lo|la|l'|i|gli|le] {task_name:name} di {days} (giorno|giorni)" + - "(rimanda|rinvia|posticipa) {task_name:name} di {days} (giorno|giorni)" - sentences: - - "(rimanda|rinvia|posticipa) [il|lo|la|l'|i|gli|le] {task_name:name} di una settimana" + - "(rimanda|rinvia|posticipa) {task_name:name} di una settimana" slots: days: 7 MaintenanceSupporterSkipTask: data: - sentences: - - "salta [il|lo|la|l'|i|gli|le] {task_name:name} [per questa volta]" + - "salta {task_name:name} per questa volta" + - "salta (la manutenzione|l'attività) {task_name:name} [per questa volta]" - "salta questo giro (di|del|della|dei|delle) {task_name:name}" + MaintenanceSupporterRecordReading: + data: + - sentences: + - "(registra|annota|segna) [una] lettura di {reading_value:value} (per|su) {task_name:name}" + - "la lettura (di|del|della|dello|dell') {task_name:name} è [di] {reading_value:value}" + - "{task_name:name} segna {reading_value:value}" + MaintenanceSupporterAddNote: + data: + - sentences: + - "aggiungi una nota (a|al|alla|allo|all'|per) {task_name:name} (che dice|dicendo) [che] {note_text:note}" + - "annota (per|su) {task_name:name} che {note_text:note}" + MaintenanceSupporterWhoseTurn: + data: + - sentences: + - "a chi tocca [fare] {task_name:name}" + - "chi (deve fare|si occupa di|si occupa del|si occupa della) {task_name:name}" + - "chi è responsabile (di|del|della|dello|dell') {task_name:name}" + MaintenanceSupporterShoppingList: + data: + - sentences: + - "cosa (dobbiamo|bisogna) (comprare|ricomprare)" + - "cosa (sta finendo|va ricomprato)" + - "quali (ricambi|pezzi|materiali di consumo|scorte) (mancano|stanno finendo|servono)" + MaintenanceSupporterBoughtPart: + data: + - sentences: + - "(ho|abbiamo) comprato {quantity} {part_name:name}" + - "(ho|abbiamo) comprato (altri|altre|nuovi|nuove) {part_name:name}" + - "aggiungi {quantity} {part_name:name} alle scorte" + MaintenanceSupporterLowBatteries: + data: + - sentences: + - "quali batterie sono (scariche|quasi scariche|basse)" + - "ci sono batterie (scariche|basse|da cambiare)" + - "quali (pile|batterie) (devo|bisogna) (cambiare|sostituire)" + MaintenanceSupporterUndo: + data: + - sentences: + - "annulla (l'ultima azione|l'ultima modifica)" + - "annullalo" + - "è stato un errore" lists: - # NON «name»: è la lista integrata dei nomi delle ENTITÀ di Home Assistant, - # che l'agente predefinito risolve prima di chiamare il gestore — ogni frase - # che la usava rispondeva «non conosco nessun dispositivo…» invece di - # arrivare fino a noi. L'alias {lista:slot} fa comunque arrivare il valore - # nello slot «name». + # Non 'name': è la lista integrata dei nomi di ENTITÀ di Home Assistant (vedi + # il file inglese). {area} è la lista delle aree di Home Assistant. task_name: wildcard: true part_name: wildcard: true + reading_value: + wildcard: true + note_text: + wildcard: true days: range: from: 1 to: 365 + quantity: + range: + from: 1 + to: 100 +expansion_rules: + # Prefisso proprio: queste regole si fondono con quelle di Home Assistant. + ms_il: "[il |lo |la |l'|i |gli |le ]" diff --git a/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml index 945edca7..d0483fd6 100644 --- a/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml +++ b/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml @@ -10,6 +10,13 @@ # Je eigen kopie aanpassen mag: zodra de inhoud niet meer overeenkomt met wat # wij geschreven hebben, blijven updates ervan af (en de instelling weer # uitzetten verwijdert hem ook niet). +# +# Eigen zinnen WINNEN van de ingebouwde zinnen van Home Assistant zodra beide +# passen. Een werkwoord dat de ingebouwde zinnen ook gebruiken (overslaan, op +# stil zetten, markeren) met een vrije naam erachter zou "sla dit nummer over" +# inslikken — zulke zinnen dragen daarom een onderhoudswoord (onderhoud, taak, +# herinneringen, "deze keer"). Een test legt elke ingebouwde zin naast dit +# bestand. language: "nl" intents: MaintenanceSupporterListTasks: @@ -19,8 +26,33 @@ intents: - "wat moet er onderhouden worden" - "is er [nog] onderhoud te doen" - "welke (klusjes|klussen|taken) staan open" - # Bereik: degene die vraagt. HA geeft aan de handler door wie er sprak, - # dus dit antwoordt met DIENS taken (inclusief zijn beurt in de rotatie). + - sentences: + - "wat is (te laat|achterstallig|over tijd)" + - "welk onderhoud is (te laat|achterstallig)" + - "welke (taken|klusjes|klussen) zijn (te laat|achterstallig)" + slots: + status: "overdue" + - sentences: + - "wat moet er vandaag gebeuren" + - "welk onderhoud staat [er] vandaag (gepland|open)" + slots: + within_days: 0 + - sentences: + - "wat moet er morgen gebeuren" + slots: + within_days: 1 + - sentences: + - "wat moet er deze week gebeuren" + - "welk onderhoud staat [er] deze week (gepland|open)" + slots: + within_days: 7 + - sentences: + - "wat moet er deze maand gebeuren" + slots: + within_days: 30 + - sentences: + - "wat moet er in [de|het] {area} gebeuren" + - "welk onderhoud is in [de|het] {area} nodig" - sentences: - "wat moet ik [nog] doen" - "wat zijn mijn (taken|klusjes|klussen)" @@ -28,8 +60,6 @@ intents: - "heb ik [nog] (iets|wat) te doen" slots: scope: "mine" - # Bereik: de kamer waaruit gevraagd wordt, afgeleid uit het gebied van de - # satelliet — zo antwoordt een wandtablet voor zijn eigen kamer. - sentences: - "wat moet er hier gebeuren" - "welk onderhoud is (hier|in deze kamer) nodig" @@ -39,58 +69,111 @@ intents: MaintenanceSupporterCompleteTask: data: - sentences: - - "ik heb [de|het] {task_name:name} (gedaan|afgerond)" - - "markeer [de|het] {task_name:name} als (gedaan|klaar|afgerond)" - - "[de|het] {task_name:name} is (klaar|gedaan)" + - "ik heb {task_name:name} (gedaan|afgerond)" + - "markeer (taak|onderhoud|klus) {task_name:name} als (gedaan|klaar|afgerond)" + - " {task_name:name} is (klaar|gedaan|afgerond)" MaintenanceSupporterTaskInstructions: data: - sentences: - - "hoe doe ik [de|het] {task_name:name}" - - "(instructies|uitleg) voor [de|het] {task_name:name}" - - "wat heb ik nodig voor [de|het] {task_name:name}" + - "hoe doe ik {task_name:name}" + - "(instructies|uitleg) voor {task_name:name}" + - "wat heb ik nodig voor {task_name:name}" MaintenanceSupporterTaskDue: data: - sentences: - - "wanneer moet [de|het] {task_name:name} (gebeuren|gedaan worden)" - - "wanneer is [de|het] {task_name:name} aan de beurt" + - "wanneer moet {task_name:name} (gebeuren|gedaan worden)" + - "wanneer is {task_name:name} aan de beurt" MaintenanceSupporterSnoozeTask: data: - sentences: - - "zet [de|het] {task_name:name} op stil" - - "(pauzeer|stop) de herinneringen voor [de|het] {task_name:name}" + - "zet de herinneringen (voor|van) {task_name:name} op stil" + - "(pauzeer|stop) de herinneringen (voor|van) {task_name:name}" + - "zet (taak|onderhoud) {task_name:name} op stil" MaintenanceSupporterPartStock: data: - sentences: - "hoeveel {part_name:name} hebben we [nog]" - "hoeveel {part_name:name} (is|zijn) er nog" - - "wat is de voorraad [van] [de|het] {part_name:name}" + - "wat is de voorraad [van] {part_name:name}" MaintenanceSupporterPostponeTask: data: - sentences: - - "stel [de|het] {task_name:name} {days} dag[en] uit" - - "schuif [de|het] {task_name:name} {days} dag[en] op" - - "verplaats [de|het] {task_name:name} met {days} dag[en]" + - "stel {task_name:name} {days} dag[en] uit" + - "schuif {task_name:name} {days} dag[en] op" + - "verplaats {task_name:name} met {days} dag[en]" - sentences: - - "stel [de|het] {task_name:name} een week uit" - - "schuif [de|het] {task_name:name} een week op" + - "stel {task_name:name} een week uit" + - "schuif {task_name:name} een week op" slots: days: 7 MaintenanceSupporterSkipTask: data: - sentences: - - "sla [de|het] {task_name:name} [deze keer] over" - - "sla deze (ronde|beurt) van [de|het] {task_name:name} over" + - "sla {task_name:name} deze keer over" + - "sla (taak|onderhoud|klus) {task_name:name} [deze keer] over" + - "sla deze (ronde|beurt) van {task_name:name} over" + MaintenanceSupporterRecordReading: + data: + - sentences: + - "(noteer|registreer) [een] (meterstand|stand|meting) van {reading_value:value} (voor|bij) {task_name:name}" + - "de (meterstand|stand) van {task_name:name} is {reading_value:value}" + MaintenanceSupporterAddNote: + data: + - sentences: + - "noteer (bij|voor) {task_name:name} dat {note_text:note}" + - "maak een notitie (bij|voor) {task_name:name} dat {note_text:note}" + MaintenanceSupporterWhoseTurn: + data: + - sentences: + - "wie is er aan de beurt (voor|met) {task_name:name}" + - "wie moet {task_name:name} doen" + - "wie is verantwoordelijk voor {task_name:name}" + MaintenanceSupporterShoppingList: + data: + - sentences: + - "wat moeten we (kopen|bijkopen|halen)" + - "wat moet er (gekocht|bijgekocht|besteld) worden" + - "welke (onderdelen|reserveonderdelen|voorraden|verbruiksartikelen) (zijn bijna op|raken op|moeten we kopen)" + - "wat raakt er op" + MaintenanceSupporterBoughtPart: + data: + - sentences: + - "(ik heb|we hebben) {quantity} [nieuwe] {part_name:name} gekocht" + - "(ik heb|we hebben) nieuwe {part_name:name} gekocht" + - "voeg {quantity} {part_name:name} toe aan de voorraad" + MaintenanceSupporterLowBatteries: + data: + - sentences: + - "welke batterijen zijn (bijna leeg|leeg|zwak)" + - "zijn er batterijen (bijna leeg|leeg|zwak)" + - "welke batterijen (moet ik|moeten we) (vervangen|wisselen)" + MaintenanceSupporterUndo: + data: + - sentences: + - "maak (dat|dit|de laatste actie) ongedaan" + - "draai (dat|dit|de laatste actie) terug" + - "dat was een fout" lists: - # NIET "name": dat is de ingebouwde lijst met ENTITEITSnamen van Home - # Assistant, die de standaardagent al oplost voordat onze handler draait — - # elke zin die hem gebruikte antwoordde "ik ken geen apparaat dat zo heet…" - # in plaats van ons te bereiken. Het alias {lijst:slot} laat de waarde - # alsnog in het slot "name" aankomen. + # Niet 'name': dat is de ingebouwde lijst met ENTITEITSnamen van Home + # Assistant (zie het Engelse bestand). {area} is de gebiedenlijst van Home + # Assistant. task_name: wildcard: true part_name: wildcard: true + reading_value: + wildcard: true + note_text: + wildcard: true days: range: from: 1 to: 365 + quantity: + range: + from: 1 + to: 100 +expansion_rules: + # Eigen voorvoegsel: deze regels worden samengevoegd met die van Home + # Assistant. + ms_de: "[de|het]" diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/cs.json b/custom_components/maintenance_supporter/assist_sentences/responses/cs.json index 3a006b28..93fe595a 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/cs.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/cs.json @@ -13,7 +13,7 @@ "skipped": "Tento cyklus úkolu '{task}' u zařízení {object} je přeskočený. Další termín je {date}.", "skip_disabled": "Přeskakování je pro '{task}' vypnuto.", "not_found": "Nenašel jsem úkol údržby, který by odpovídal '{name}'.", - "ambiguous": "Tomu odpovídá víc úkolů: {candidates}. Upřesni to, prosím.", + "ambiguous": "Tomu odpovídá víc úkolů: {candidates}. Řekni který, například „{example}“.", "too_early": "Úkol '{task}' se dá dokončit až blíž k termínu.", "st_overdue": "zpoždění {days} dní", "st_overdue_one": "zpoždění jeden den", @@ -43,5 +43,49 @@ "st_phase": " — další krok: {phase}", "st_priority": ", vysoká priorita", "completed_phase": "'{task}' na {object} dokončeno — krok: {phase}. Příště: {next}.", - "guide_phase": "Aktuální krok: {phase} (krok {index} z {count})." + "guide_phase": "Aktuální krok: {phase} (krok {index} z {count}).", + "part_ambiguous": "Tomu odpovídá víc dílů: {candidates}. Řekni který, například „{example}“.", + "st_no_date": "bez termínu", + "st_overdue_plain": "po termínu", + "st_due_soon_plain": "brzy na řadě", + "months": "ledna,února,března,dubna,května,června,července,srpna,září,října,listopadu,prosince", + "date_format": "{day}. {month}", + "date_format_year": "{day}. {month} {year}", + "skipped_no_date": "Tento cyklus úkolu '{task}' u zařízení {object} byl přeskočen.", + "snoozed_one": "Připomínky k úkolu '{task}' u zařízení {object} jsou ztlumené na hodinu.", + "guide_checklist_one": "jeden krok kontrolního seznamu: {steps}", + "guide_none_plain": "K úkolu '{task}' u zařízení {object} nejsou uložené žádné návody, dokumenty ani díly.", + "none_due_window": "V tomto období nic nevychází.", + "none_due_area": "V oblasti {area} není potřeba žádná údržba.", + "unknown_area_name": "Oblast s názvem '{area}' neznám.", + "reading_recorded": "Odečet {value}{unit} zapsán k úkolu '{task}' u zařízení {object}.", + "reading_not_a_reading": "Úkol '{task}' nezaznamenává odečty.", + "reading_multi": "Úkol '{task}' zaznamenává více odečtů — zadej je v panelu údržby.", + "reading_unclear": "V '{value}' jsem nezachytil žádné číslo.", + "note_added": "Poznámka přidána k úkolu '{task}' u zařízení {object}.", + "note_too_long": "Poznámky úkolu '{task}' jsou plné — nejdřív je zkrať v panelu údržby.", + "note_empty": "Co má být v poznámce?", + "turn_user": "Teď je na řadě: {user} — '{task}' u zařízení {object}.", + "turn_you": "Teď jsi na řadě ty: '{task}' u zařízení {object}.", + "turn_nobody": "K úkolu '{task}' u zařízení {object} není nikdo přiřazen.", + "buy_none": "Nic nedochází — není potřeba kupovat díly.", + "buy_list": "Docházejí díly, celkem {count}: {items}.", + "buy_list_one": "Dochází jeden díl: {items}.", + "buy_item": "{part} (zbývá {stock})", + "bought": "Přidáno {qty} × {part} — teď je skladem {stock}.", + "batteries_unavailable": "Žádné baterie se nesledují.", + "batteries_none": "Žádná baterie není slabá.", + "batteries_low": "Slabé baterie, celkem {count}: {items}.", + "batteries_low_one": "Jedna baterie je slabá: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Koupit: {needs}.", + "undo_none": "Za posledních {minutes} minut od tebe není co vracet.", + "undo_changed": "Úkol '{task}' se mezitím změnil, takže ho nechávám, jak je.", + "undone_completed": "Vráceno: úkol '{task}' u zařízení {object} už není splněný.", + "undone_reading": "Vráceno: odečet k úkolu '{task}' u zařízení {object} je odstraněn.", + "undone_skipped": "Vráceno: úkol '{task}' u zařízení {object} už není přeskočený.", + "undone_postponed": "Vráceno: úkol '{task}' u zařízení {object} má zase původní termín.", + "undone_snoozed": "Vráceno: připomínky k úkolu '{task}' u zařízení {object} jsou znovu zapnuté.", + "undone_note": "Vráceno: poznámka k úkolu '{task}' u zařízení {object} je odstraněna.", + "undone_bought": "Vráceno: {qty} × {part} znovu odebráno ze skladu." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/da.json b/custom_components/maintenance_supporter/assist_sentences/responses/da.json index 64f87c5c..57c72cc4 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/da.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/da.json @@ -13,7 +13,7 @@ "skipped": "Jeg har sprunget denne omgang af '{task}' på {object} over. Næste omgang skal laves den {date}.", "skip_disabled": "Spring over er slået fra for '{task}'.", "not_found": "Jeg kunne ikke finde en vedligeholdelsesopgave, der passer på '{name}'.", - "ambiguous": "Det passer på flere opgaver: {candidates}. Vær lige lidt mere præcis.", + "ambiguous": "Det passer på flere opgaver: {candidates}. Sig hvilken, for eksempel »{example}«.", "too_early": "'{task}' kan først markeres som udført tættere på forfaldsdatoen.", "st_overdue": "{days} dage for sent", "st_overdue_one": "én dag for sent", @@ -43,5 +43,49 @@ "st_phase": " — næste trin: {phase}", "st_priority": ", høj prioritet", "completed_phase": "'{task}' på {object} udført — trin: {phase}. Næste gang: {next}.", - "guide_phase": "Aktuelt trin: {phase} (trin {index} af {count})." + "guide_phase": "Aktuelt trin: {phase} (trin {index} af {count}).", + "part_ambiguous": "Det passer på flere reservedele: {candidates}. Sig hvilken, for eksempel »{example}«.", + "st_no_date": "uden forfaldsdato", + "st_overdue_plain": "forsinket", + "st_due_soon_plain": "snart", + "months": "januar,februar,marts,april,maj,juni,juli,august,september,oktober,november,december", + "date_format": "{day}. {month}", + "date_format_year": "{day}. {month} {year}", + "skipped_no_date": "Denne runde af '{task}' på {object} er sprunget over.", + "snoozed_one": "Påmindelser for '{task}' på {object} er sat på pause i en time.", + "guide_checklist_one": "ét tjeklistetrin: {steps}", + "guide_none_plain": "Der er ingen gemte vejledninger, dokumenter eller reservedele til '{task}' på {object}.", + "none_due_window": "Intet forfalder i den periode.", + "none_due_area": "Intet skal vedligeholdes i {area}.", + "unknown_area_name": "Jeg kender ikke noget område, der hedder '{area}'.", + "reading_recorded": "Aflæsning {value}{unit} registreret for '{task}' på {object}.", + "reading_not_a_reading": "'{task}' registrerer ingen aflæsning.", + "reading_multi": "'{task}' registrerer flere aflæsninger — indtast dem i vedligeholdelsespanelet.", + "reading_unclear": "Jeg fangede ikke noget tal i '{value}'.", + "note_added": "Noten er tilføjet til '{task}' på {object}.", + "note_too_long": "Noterne til '{task}' er fulde — gør dem kortere i vedligeholdelsespanelet først.", + "note_empty": "Hvad skal der stå i noten?", + "turn_user": "{user} har tur: '{task}' på {object}.", + "turn_you": "Det er din tur: '{task}' på {object}.", + "turn_nobody": "Ingen er tildelt '{task}' på {object}.", + "buy_none": "Intet er ved at slippe op — der skal ikke købes reservedele.", + "buy_list": "{count} reservedele er ved at slippe op: {items}.", + "buy_list_one": "Én reservedel er ved at slippe op: {items}.", + "buy_item": "{part} ({stock} tilbage)", + "bought": "{qty} × {part} lagt på lager — nu {stock} på lager.", + "batteries_unavailable": "Ingen batterier bliver overvåget.", + "batteries_none": "Ingen batterier er ved at være flade.", + "batteries_low": "{count} batterier er ved at være flade: {items}.", + "batteries_low_one": "Ét batteri er ved at være fladt: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Skal købes: {needs}.", + "undo_none": "Der er intet fra dig at fortryde fra de sidste {minutes} minutter.", + "undo_changed": "'{task}' er ændret siden, så jeg lader den være.", + "undone_completed": "Fortrudt: '{task}' på {object} er ikke længere udført.", + "undone_reading": "Fortrudt: aflæsningen for '{task}' på {object} er fjernet.", + "undone_skipped": "Fortrudt: '{task}' på {object} er ikke længere sprunget over.", + "undone_postponed": "Fortrudt: '{task}' på {object} har sin gamle dato igen.", + "undone_snoozed": "Fortrudt: påmindelserne for '{task}' på {object} er slået til igen.", + "undone_note": "Fortrudt: noten på '{task}' ({object}) er fjernet.", + "undone_bought": "Fortrudt: {qty} × {part} taget ud af lageret igen." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/de.json b/custom_components/maintenance_supporter/assist_sentences/responses/de.json index f27a3e63..bc4ef0a7 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/de.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/de.json @@ -13,7 +13,7 @@ "skipped": "Dieser Durchgang von '{task}' an {object} wurde übersprungen. Der nächste ist am {date} fällig.", "skip_disabled": "Überspringen ist für '{task}' ausgeschaltet.", "not_found": "Ich habe keine Wartungsaufgabe zu '{name}' gefunden.", - "ambiguous": "Das passt auf mehrere Aufgaben: {candidates}. Bitte formuliere genauer.", + "ambiguous": "Das passt auf mehrere Aufgaben: {candidates}. Sag, welche du meinst, zum Beispiel „{example}“.", "too_early": "'{task}' kann erst näher am Fälligkeitstermin erledigt werden.", "st_overdue": "seit {days} Tagen überfällig", "st_overdue_one": "seit einem Tag überfällig", @@ -43,5 +43,49 @@ "st_phase": " — nächster Schritt: {phase}", "st_priority": ", hohe Priorität", "completed_phase": "'{task}' an {object} erledigt — Schritt: {phase}. Beim nächsten Mal: {next}.", - "guide_phase": "Aktueller Schritt: {phase} (Schritt {index} von {count})." + "guide_phase": "Aktueller Schritt: {phase} (Schritt {index} von {count}).", + "part_ambiguous": "Das passt auf mehrere Ersatzteile: {candidates}. Sag, welches du meinst, zum Beispiel „{example}“.", + "st_no_date": "ohne Fälligkeitsdatum", + "st_overdue_plain": "überfällig", + "st_due_soon_plain": "bald fällig", + "months": "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", + "date_format": "{day}. {month}", + "date_format_year": "{day}. {month} {year}", + "skipped_no_date": "Dieser Durchgang von '{task}' an {object} wurde übersprungen.", + "snoozed_one": "Erinnerungen für '{task}' an {object} für eine Stunde stummgeschaltet.", + "guide_checklist_one": "ein Checklisten-Schritt: {steps}", + "guide_none_plain": "Zu '{task}' an {object} sind keine Anleitungen, Dokumente oder Ersatzteile hinterlegt.", + "none_due_window": "In diesem Zeitraum ist nichts fällig.", + "none_due_area": "Im Bereich {area} ist keine Wartung fällig.", + "unknown_area_name": "Einen Bereich namens '{area}' kenne ich nicht.", + "reading_recorded": "{value}{unit} für '{task}' an {object} eingetragen.", + "reading_not_a_reading": "'{task}' erfasst keinen Messwert.", + "reading_multi": "'{task}' erfasst mehrere Messwerte — trag sie im Wartungs-Panel ein.", + "reading_unclear": "In '{value}' habe ich keine Zahl erkannt.", + "note_added": "Notiz zu '{task}' an {object} hinzugefügt.", + "note_too_long": "Die Notizen zu '{task}' sind voll — kürze sie zuerst im Wartungs-Panel.", + "note_empty": "Was soll in der Notiz stehen?", + "turn_user": "{user} ist dran: '{task}' an {object}.", + "turn_you": "Du bist dran: '{task}' an {object}.", + "turn_nobody": "Für '{task}' an {object} ist niemand eingetragen.", + "buy_none": "Nichts wird knapp — es müssen keine Ersatzteile gekauft werden.", + "buy_list": "{count} Ersatzteile werden knapp: {items}.", + "buy_list_one": "Ein Ersatzteil wird knapp: {items}.", + "buy_item": "{part} (noch {stock})", + "bought": "{qty} × {part} eingebucht — jetzt {stock} auf Lager.", + "batteries_unavailable": "Es werden keine Batterien überwacht.", + "batteries_none": "Keine Batterie ist schwach.", + "batteries_low": "{count} Batterien sind schwach: {items}.", + "batteries_low_one": "Eine Batterie ist schwach: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Zu kaufen: {needs}.", + "undo_none": "Von dir gibt es aus den letzten {minutes} Minuten nichts rückgängig zu machen.", + "undo_changed": "'{task}' hat sich inzwischen geändert — ich lasse es, wie es ist.", + "undone_completed": "Rückgängig gemacht: '{task}' an {object} ist nicht mehr erledigt.", + "undone_reading": "Rückgängig gemacht: Der Messwert für '{task}' an {object} ist entfernt.", + "undone_skipped": "Rückgängig gemacht: '{task}' an {object} ist nicht mehr übersprungen.", + "undone_postponed": "Rückgängig gemacht: '{task}' an {object} hat wieder den alten Termin.", + "undone_snoozed": "Rückgängig gemacht: Die Erinnerungen für '{task}' an {object} sind wieder an.", + "undone_note": "Rückgängig gemacht: Die Notiz zu '{task}' an {object} ist entfernt.", + "undone_bought": "Rückgängig gemacht: {qty} × {part} wieder aus dem Bestand genommen." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/en.json b/custom_components/maintenance_supporter/assist_sentences/responses/en.json index 795bcea0..d12de4c0 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/en.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/en.json @@ -13,7 +13,7 @@ "skipped": "Skipped this cycle of '{task}' on {object}. The next one is due {date}.", "skip_disabled": "Skipping is turned off for '{task}'.", "not_found": "I couldn't find a maintenance task matching '{name}'.", - "ambiguous": "That matches several tasks: {candidates}. Please be more specific.", + "ambiguous": "That matches several tasks: {candidates}. Say which one, for example “{example}”.", "too_early": "'{task}' can only be completed closer to its due date.", "st_overdue": "{days} days overdue", "st_overdue_one": "1 day overdue", @@ -43,5 +43,49 @@ "st_phase": " — next step: {phase}", "st_priority": ", high priority", "completed_phase": "Completed '{task}' on {object} — step done: {phase}. Next time: {next}.", - "guide_phase": "Current step: {phase} (step {index} of {count})." + "guide_phase": "Current step: {phase} (step {index} of {count}).", + "part_ambiguous": "That matches several spare parts: {candidates}. Say which one, for example “{example}”.", + "st_no_date": "no due date", + "st_overdue_plain": "overdue", + "st_due_soon_plain": "due soon", + "months": "January,February,March,April,May,June,July,August,September,October,November,December", + "date_format": "{month} {day}", + "date_format_year": "{month} {day}, {year}", + "skipped_no_date": "Skipped this cycle of '{task}' on {object}.", + "snoozed_one": "Snoozed reminders for '{task}' on {object} for one hour.", + "guide_checklist_one": "one checklist step: {steps}", + "guide_none_plain": "There are no stored instructions, documents or spare parts for '{task}' on {object}.", + "none_due_window": "Nothing is due in that time.", + "none_due_area": "Nothing needs maintenance in {area}.", + "unknown_area_name": "I don't know an area called '{area}'.", + "reading_recorded": "Recorded {value}{unit} for '{task}' on {object}.", + "reading_not_a_reading": "'{task}' doesn't record a reading.", + "reading_multi": "'{task}' records several readings — enter them in the Maintenance panel.", + "reading_unclear": "I didn't catch a number in '{value}'.", + "note_added": "Added the note to '{task}' on {object}.", + "note_too_long": "The notes of '{task}' are full — shorten them in the Maintenance panel first.", + "note_empty": "What should the note say?", + "turn_user": "It's {user}'s turn: '{task}' on {object}.", + "turn_you": "It's your turn: '{task}' on {object}.", + "turn_nobody": "Nobody is assigned to '{task}' on {object}.", + "buy_none": "Nothing is running low — no spare parts need buying.", + "buy_list": "{count} spare parts are running low: {items}.", + "buy_list_one": "One spare part is running low: {items}.", + "buy_item": "{part} ({stock} left)", + "bought": "Added {qty} × {part} — {stock} in stock now.", + "batteries_unavailable": "No batteries are being tracked.", + "batteries_none": "No batteries are low.", + "batteries_low": "{count} batteries are low: {items}.", + "batteries_low_one": "One battery is low: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " To buy: {needs}.", + "undo_none": "There is nothing of yours to undo from the last {minutes} minutes.", + "undo_changed": "'{task}' has changed since, so I left it as it is.", + "undone_completed": "Undone: '{task}' on {object} is no longer completed.", + "undone_reading": "Undone: the reading for '{task}' on {object} is removed.", + "undone_skipped": "Undone: '{task}' on {object} is no longer skipped.", + "undone_postponed": "Undone: '{task}' on {object} is back on its old date.", + "undone_snoozed": "Undone: reminders for '{task}' on {object} are on again.", + "undone_note": "Undone: the note on '{task}' on {object} is removed.", + "undone_bought": "Undone: took {qty} × {part} back out of stock." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/es.json b/custom_components/maintenance_supporter/assist_sentences/responses/es.json index e786acc2..01360311 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/es.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/es.json @@ -13,7 +13,7 @@ "skipped": "Me he saltado este ciclo de '{task}' en {object}. El siguiente toca el {date}.", "skip_disabled": "Omitir está desactivado para '{task}'.", "not_found": "No he encontrado ninguna tarea de mantenimiento que coincida con '{name}'.", - "ambiguous": "Eso coincide con varias tareas: {candidates}. Concreta un poco más, por favor.", + "ambiguous": "Eso coincide con varias tareas: {candidates}. Di cuál, por ejemplo «{example}».", "too_early": "'{task}' solo se puede completar cuando se acerque su fecha prevista.", "st_overdue": "{days} días de retraso", "st_overdue_one": "un día de retraso", @@ -43,5 +43,49 @@ "st_phase": " — siguiente paso: {phase}", "st_priority": ", prioridad alta", "completed_phase": "'{task}' en {object} completado — paso: {phase}. La próxima vez: {next}.", - "guide_phase": "Paso actual: {phase} (paso {index} de {count})." + "guide_phase": "Paso actual: {phase} (paso {index} de {count}).", + "part_ambiguous": "Eso coincide con varios repuestos: {candidates}. Di cuál, por ejemplo «{example}».", + "st_no_date": "sin fecha", + "st_overdue_plain": "atrasada", + "st_due_soon_plain": "pronto", + "months": "enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre", + "date_format": "{day} de {month}", + "date_format_year": "{day} de {month} de {year}", + "skipped_no_date": "Se ha saltado esta ronda de '{task}' en {object}.", + "snoozed_one": "Avisos de '{task}' en {object} silenciados durante una hora.", + "guide_checklist_one": "un paso de la lista: {steps}", + "guide_none_plain": "No hay instrucciones, documentos ni repuestos guardados para '{task}' en {object}.", + "none_due_window": "No toca nada en ese plazo.", + "none_due_area": "No hay mantenimiento pendiente en {area}.", + "unknown_area_name": "No conozco ninguna zona llamada '{area}'.", + "reading_recorded": "Lectura de {value}{unit} registrada para '{task}' en {object}.", + "reading_not_a_reading": "'{task}' no registra lecturas.", + "reading_multi": "'{task}' registra varias lecturas: introdúcelas en el panel de mantenimiento.", + "reading_unclear": "No he entendido ningún número en '{value}'.", + "note_added": "Nota añadida a '{task}' en {object}.", + "note_too_long": "Las notas de '{task}' están llenas: acórtalas primero en el panel de mantenimiento.", + "note_empty": "¿Qué debe decir la nota?", + "turn_user": "Le toca a {user}: '{task}' en {object}.", + "turn_you": "Te toca a ti: '{task}' en {object}.", + "turn_nobody": "Nadie tiene asignada '{task}' en {object}.", + "buy_none": "No se está acabando nada: no hay que comprar repuestos.", + "buy_list": "Se están acabando {count} repuestos: {items}.", + "buy_list_one": "Se está acabando un repuesto: {items}.", + "buy_item": "{part} (quedan {stock})", + "bought": "{qty} × {part} añadidos: ahora hay {stock} en stock.", + "batteries_unavailable": "No se está siguiendo ninguna batería.", + "batteries_none": "Ninguna batería está baja.", + "batteries_low": "{count} baterías están bajas: {items}.", + "batteries_low_one": "Una batería está baja: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Para comprar: {needs}.", + "undo_none": "No hay nada tuyo que deshacer de los últimos {minutes} minutos.", + "undo_changed": "'{task}' ha cambiado desde entonces, así que lo dejo como está.", + "undone_completed": "Deshecho: '{task}' en {object} ya no está completada.", + "undone_reading": "Deshecho: se ha quitado la lectura de '{task}' en {object}.", + "undone_skipped": "Deshecho: '{task}' en {object} ya no está saltada.", + "undone_postponed": "Deshecho: '{task}' en {object} vuelve a su fecha anterior.", + "undone_snoozed": "Deshecho: los avisos de '{task}' en {object} vuelven a estar activos.", + "undone_note": "Deshecho: se ha quitado la nota de '{task}' en {object}.", + "undone_bought": "Deshecho: {qty} × {part} retirados del stock." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/fi.json b/custom_components/maintenance_supporter/assist_sentences/responses/fi.json index 4933b203..78da77d5 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/fi.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/fi.json @@ -13,7 +13,7 @@ "skipped": "Ohitin tehtävän '{task}' tämän kerran kohteessa {object}. Seuraava ajankohta: {date}.", "skip_disabled": "Ohittaminen on poistettu käytöstä kohteelle '{task}'.", "not_found": "En löytänyt huoltotehtävää, joka vastaisi hakua '{name}'.", - "ambiguous": "Tämä vastaa useaa tehtävää: {candidates}. Tarkenna vielä hieman.", + "ambiguous": "Tämä vastaa useaa tehtävää: {candidates}. Sano mikä, esimerkiksi ”{example}”.", "too_early": "Tehtävän '{task}' voi kuitata tehdyksi vasta lähempänä sen ajankohtaa.", "st_overdue": "{days} päivää myöhässä", "st_overdue_one": "päivän myöhässä", @@ -43,5 +43,49 @@ "st_phase": " — seuraava vaihe: {phase}", "st_priority": ", korkea prioriteetti", "completed_phase": "'{task}' ({object}) valmis — vaihe: {phase}. Ensi kerralla: {next}.", - "guide_phase": "Nykyinen vaihe: {phase} (vaihe {index}/{count})." + "guide_phase": "Nykyinen vaihe: {phase} (vaihe {index}/{count}).", + "part_ambiguous": "Tämä vastaa useaa varaosaa: {candidates}. Sano mikä, esimerkiksi ”{example}”.", + "st_no_date": "ei eräpäivää", + "st_overdue_plain": "myöhässä", + "st_due_soon_plain": "pian vuorossa", + "months": "tammikuuta,helmikuuta,maaliskuuta,huhtikuuta,toukokuuta,kesäkuuta,heinäkuuta,elokuuta,syyskuuta,lokakuuta,marraskuuta,joulukuuta", + "date_format": "{day}. {month}", + "date_format_year": "{day}. {month} {year}", + "skipped_no_date": "Ohitin tehtävän '{task}' tämän kerran kohteessa {object}.", + "snoozed_one": "Muistutukset tehtävästä '{task}' kohteessa {object} on vaimennettu tunniksi.", + "guide_checklist_one": "yksi tarkistuslistan kohta: {steps}", + "guide_none_plain": "Tehtävälle '{task}' kohteessa {object} ei ole tallennettu ohjeita, dokumentteja eikä varaosia.", + "none_due_window": "Sillä aikavälillä ei ole mitään vuorossa.", + "none_due_area": "Alueella {area} ei tarvita huoltoa.", + "unknown_area_name": "En tunne aluetta nimeltä '{area}'.", + "reading_recorded": "Kirjasin lukeman {value}{unit} tehtävälle '{task}' kohteessa {object}.", + "reading_not_a_reading": "Tehtävä '{task}' ei kirjaa lukemia.", + "reading_multi": "Tehtävä '{task}' kirjaa useita lukemia — syötä ne huoltopaneelissa.", + "reading_unclear": "En erottanut lukua kohdasta '{value}'.", + "note_added": "Lisäsin muistiinpanon tehtävälle '{task}' kohteessa {object}.", + "note_too_long": "Tehtävän '{task}' muistiinpanot ovat täynnä — lyhennä niitä ensin huoltopaneelissa.", + "note_empty": "Mitä muistiinpanoon kirjoitetaan?", + "turn_user": "Vuorossa on {user}: '{task}' kohteessa {object}.", + "turn_you": "Sinä olet vuorossa: '{task}' kohteessa {object}.", + "turn_nobody": "Tehtävälle '{task}' kohteessa {object} ei ole määritetty ketään.", + "buy_none": "Mikään ei ole loppumassa — varaosia ei tarvitse ostaa.", + "buy_list": "Varaosia on loppumassa, yhteensä {count}: {items}.", + "buy_list_one": "Yksi varaosa on loppumassa: {items}.", + "buy_item": "{part} (jäljellä {stock})", + "bought": "Lisäsin varastoon {qty} × {part} — nyt varastossa {stock}.", + "batteries_unavailable": "Paristoja ei seurata.", + "batteries_none": "Yksikään paristo ei ole heikko.", + "batteries_low": "Heikkoja paristoja on {count}: {items}.", + "batteries_low_one": "Yksi paristo on heikko: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Ostettavaa: {needs}.", + "undo_none": "Sinulta ei ole mitään peruttavaa viimeisen {minutes} minuutin ajalta.", + "undo_changed": "Tehtävä '{task}' on muuttunut sen jälkeen, joten jätän sen ennalleen.", + "undone_completed": "Peruttu: tehtävä '{task}' kohteessa {object} ei ole enää tehty.", + "undone_reading": "Peruttu: tehtävän '{task}' lukema kohteessa {object} on poistettu.", + "undone_skipped": "Peruttu: tehtävää '{task}' kohteessa {object} ei ole enää ohitettu.", + "undone_postponed": "Peruttu: tehtävä '{task}' kohteessa {object} on taas alkuperäisenä päivänä.", + "undone_snoozed": "Peruttu: tehtävän '{task}' muistutukset kohteessa {object} ovat taas päällä.", + "undone_note": "Peruttu: tehtävän '{task}' muistiinpano kohteessa {object} on poistettu.", + "undone_bought": "Peruttu: {qty} × {part} otettu taas pois varastosta." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/fr.json b/custom_components/maintenance_supporter/assist_sentences/responses/fr.json index d26b11c3..4af90ca6 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/fr.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/fr.json @@ -13,7 +13,7 @@ "skipped": "J'ai sauté ce cycle de '{task}' sur {object}. Le prochain est à faire le {date}.", "skip_disabled": "Ignorer est désactivé pour '{task}'.", "not_found": "Je n'ai pas trouvé de tâche d'entretien qui corresponde à '{name}'.", - "ambiguous": "Ça correspond à plusieurs tâches : {candidates}. Sois un peu plus précis, s'il te plaît.", + "ambiguous": "Ça correspond à plusieurs tâches : {candidates}. Dis laquelle, par exemple « {example} ».", "too_early": "Tu ne pourras valider '{task}' que plus près de sa date d'échéance.", "st_overdue": "{days} jours de retard", "st_overdue_one": "un jour de retard", @@ -43,5 +43,49 @@ "st_phase": " — prochaine étape : {phase}", "st_priority": ", priorité haute", "completed_phase": "« {task} » sur {object} terminé — étape : {phase}. La prochaine fois : {next}.", - "guide_phase": "Étape actuelle : {phase} (étape {index} sur {count})." + "guide_phase": "Étape actuelle : {phase} (étape {index} sur {count}).", + "part_ambiguous": "Ça correspond à plusieurs pièces : {candidates}. Dis laquelle, par exemple « {example} ».", + "st_no_date": "sans échéance", + "st_overdue_plain": "en retard", + "st_due_soon_plain": "bientôt à faire", + "months": "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "Ce cycle de '{task}' sur {object} a été sauté.", + "snoozed_one": "Rappels pour '{task}' sur {object} mis en pause pendant une heure.", + "guide_checklist_one": "une étape de checklist : {steps}", + "guide_none_plain": "Aucune instruction, aucun document ni aucune pièce n'est enregistré pour '{task}' sur {object}.", + "none_due_window": "Rien n'est prévu sur cette période.", + "none_due_area": "Aucun entretien n'est à faire dans {area}.", + "unknown_area_name": "Je ne connais pas de pièce appelée '{area}'.", + "reading_recorded": "Relevé {value}{unit} enregistré pour '{task}' sur {object}.", + "reading_not_a_reading": "'{task}' n'enregistre pas de relevé.", + "reading_multi": "'{task}' enregistre plusieurs relevés — saisis-les dans le panneau Maintenance.", + "reading_unclear": "Je n'ai pas compris de nombre dans '{value}'.", + "note_added": "Note ajoutée à '{task}' sur {object}.", + "note_too_long": "Les notes de '{task}' sont pleines — raccourcis-les d'abord dans le panneau Maintenance.", + "note_empty": "Que doit dire la note ?", + "turn_user": "C'est au tour de {user} : '{task}' sur {object}.", + "turn_you": "C'est ton tour : '{task}' sur {object}.", + "turn_nobody": "Personne n'est assigné à '{task}' sur {object}.", + "buy_none": "Rien ne manque — aucune pièce à acheter.", + "buy_list": "{count} pièces arrivent en rupture : {items}.", + "buy_list_one": "Une pièce arrive en rupture : {items}.", + "buy_item": "{part} ({stock} restant)", + "bought": "{qty} × {part} ajouté au stock — {stock} en stock maintenant.", + "batteries_unavailable": "Aucune pile n'est suivie.", + "batteries_none": "Aucune pile n'est faible.", + "batteries_low": "{count} piles sont faibles : {items}.", + "batteries_low_one": "Une pile est faible : {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " À acheter : {needs}.", + "undo_none": "Tu n'as rien à annuler des {minutes} dernières minutes.", + "undo_changed": "'{task}' a changé depuis, je le laisse tel quel.", + "undone_completed": "Annulé : '{task}' sur {object} n'est plus fait.", + "undone_reading": "Annulé : le relevé de '{task}' sur {object} est supprimé.", + "undone_skipped": "Annulé : '{task}' sur {object} n'est plus sauté.", + "undone_postponed": "Annulé : '{task}' sur {object} retrouve sa date d'origine.", + "undone_snoozed": "Annulé : les rappels pour '{task}' sur {object} sont réactivés.", + "undone_note": "Annulé : la note sur '{task}' ({object}) est supprimée.", + "undone_bought": "Annulé : {qty} × {part} retiré du stock." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/hi.json b/custom_components/maintenance_supporter/assist_sentences/responses/hi.json index ece55eda..9cc86f11 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/hi.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/hi.json @@ -13,7 +13,7 @@ "skipped": "{object} पर '{task}' का यह चक्र छोड़ दिया है। अगली बार {date} को करना है।", "skip_disabled": "'{task}' के लिए छोड़ना बंद है।", "not_found": "'{name}' से मिलता-जुलता कोई मेंटेनेंस काम नहीं मिला।", - "ambiguous": "इससे कई काम मेल खाते हैं: {candidates}। थोड़ा और साफ़ बताइए।", + "ambiguous": "इससे कई काम मेल खाते हैं: {candidates}। बताइए कौन-सा, जैसे “{example}”।", "too_early": "'{task}' को तय तारीख़ के क़रीब आने पर ही पूरा किया जा सकता है।", "st_overdue": "{days} दिन की देरी", "st_overdue_one": "एक दिन की देरी", @@ -43,5 +43,49 @@ "st_phase": " — अगला चरण: {phase}", "st_priority": ", उच्च प्राथमिकता", "completed_phase": "{object} पर '{task}' पूर्ण — चरण: {phase}। अगली बार: {next}।", - "guide_phase": "वर्तमान चरण: {phase} ({count} में से चरण {index})।" + "guide_phase": "वर्तमान चरण: {phase} ({count} में से चरण {index})।", + "part_ambiguous": "इससे कई स्पेयर पार्ट मेल खाते हैं: {candidates}। बताइए कौन-सा, जैसे “{example}”।", + "st_no_date": "कोई तारीख़ तय नहीं", + "st_overdue_plain": "देरी से", + "st_due_soon_plain": "जल्द होना है", + "months": "जनवरी,फ़रवरी,मार्च,अप्रैल,मई,जून,जुलाई,अगस्त,सितंबर,अक्टूबर,नवंबर,दिसंबर", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "{object} पर '{task}' का यह चक्र छोड़ दिया है।", + "snoozed_one": "{object} पर '{task}' के रिमाइंडर एक घंटे के लिए बंद कर दिए हैं।", + "guide_checklist_one": "चेकलिस्ट का एक चरण: {steps}", + "guide_none_plain": "{object} पर '{task}' के लिए कोई निर्देश, दस्तावेज़ या स्पेयर पार्ट सहेजा नहीं गया है।", + "none_due_window": "इस अवधि में कुछ भी तय नहीं है।", + "none_due_area": "{area} में किसी मेंटेनेंस की ज़रूरत नहीं है।", + "unknown_area_name": "'{area}' नाम का कोई क्षेत्र मुझे नहीं पता।", + "reading_recorded": "{object} पर '{task}' के लिए {value}{unit} रीडिंग दर्ज कर दी है।", + "reading_not_a_reading": "'{task}' कोई रीडिंग दर्ज नहीं करता।", + "reading_multi": "'{task}' कई रीडिंग दर्ज करता है — उन्हें मेंटेनेंस पैनल में भरिए।", + "reading_unclear": "'{value}' में मुझे कोई संख्या सुनाई नहीं दी।", + "note_added": "{object} पर '{task}' में नोट जोड़ दिया है।", + "note_too_long": "'{task}' के नोट भर चुके हैं — पहले मेंटेनेंस पैनल में उन्हें छोटा कीजिए।", + "note_empty": "नोट में क्या लिखना है?", + "turn_user": "अभी बारी है: {user} — {object} पर '{task}'।", + "turn_you": "अभी आपकी बारी है: {object} पर '{task}'।", + "turn_nobody": "{object} पर '{task}' किसी को सौंपा नहीं गया है।", + "buy_none": "कुछ भी ख़त्म नहीं हो रहा — कोई स्पेयर पार्ट ख़रीदने की ज़रूरत नहीं है।", + "buy_list": "{count} स्पेयर पार्ट ख़त्म होने वाले हैं: {items}।", + "buy_list_one": "एक स्पेयर पार्ट ख़त्म होने वाला है: {items}।", + "buy_item": "{part} ({stock} बचे)", + "bought": "{qty} × {part} स्टॉक में जोड़ दिए — अब स्टॉक में {stock} हैं।", + "batteries_unavailable": "किसी बैटरी पर नज़र नहीं रखी जा रही है।", + "batteries_none": "कोई बैटरी कमज़ोर नहीं है।", + "batteries_low": "{count} बैटरियाँ कमज़ोर हैं: {items}।", + "batteries_low_one": "एक बैटरी कमज़ोर है: {items}।", + "battery_item": "{device} ({type})", + "batteries_buy": " ख़रीदना है: {needs}।", + "undo_none": "पिछले {minutes} मिनट में आपका कोई काम पलटने के लिए नहीं है।", + "undo_changed": "'{task}' तब से बदल चुका है, इसलिए मैं उसे वैसे ही छोड़ रहा हूँ।", + "undone_completed": "पलट दिया: {object} पर '{task}' अब पूरा नहीं है।", + "undone_reading": "पलट दिया: {object} पर '{task}' की रीडिंग हटा दी गई है।", + "undone_skipped": "पलट दिया: {object} पर '{task}' अब छोड़ा हुआ नहीं है।", + "undone_postponed": "पलट दिया: {object} पर '{task}' फिर से पुरानी तारीख़ पर है।", + "undone_snoozed": "पलट दिया: {object} पर '{task}' के रिमाइंडर फिर से चालू हैं।", + "undone_note": "पलट दिया: {object} पर '{task}' का नोट हटा दिया गया है।", + "undone_bought": "पलट दिया: {qty} × {part} फिर से स्टॉक से हटा दिए।" } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/hu.json b/custom_components/maintenance_supporter/assist_sentences/responses/hu.json index 9146c3ec..c058abcc 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/hu.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/hu.json @@ -13,7 +13,7 @@ "skipped": "Kihagytam ezt a fordulót: '{task}' – {object}. A következő időpont: {date}.", "skip_disabled": "A kihagyás ki van kapcsolva ehhez: '{task}'.", "not_found": "Nem találtam ehhez illő karbantartási feladatot: '{name}'.", - "ambiguous": "Több feladatra is illik: {candidates}. Fogalmazz pontosabban.", + "ambiguous": "Több feladatra is illik: {candidates}. Mondd meg, melyikre gondolsz, például: „{example}”.", "too_early": "'{task}': ezt a feladatot csak az esedékesség közelében lehet elvégezni.", "st_overdue": "{days} napja késésben", "st_overdue_one": "egy napja késésben", @@ -43,5 +43,49 @@ "st_phase": " — következő lépés: {phase}", "st_priority": ", magas prioritás", "completed_phase": "'{task}' ({object}) elvégezve — lépés: {phase}. Legközelebb: {next}.", - "guide_phase": "Aktuális lépés: {phase} ({index}. lépés a(z) {count} közül)." + "guide_phase": "Aktuális lépés: {phase} ({index}. lépés a(z) {count} közül).", + "part_ambiguous": "Több alkatrészre is illik: {candidates}. Mondd meg, melyikre gondolsz, például: „{example}”.", + "st_no_date": "nincs határideje", + "st_overdue_plain": "késésben", + "st_due_soon_plain": "hamarosan esedékes", + "months": "január,február,március,április,május,június,július,augusztus,szeptember,október,november,december", + "date_format": "{month} {day}.", + "date_format_year": "{year}. {month} {day}.", + "skipped_no_date": "Kihagytam ezt a fordulót: '{task}' – {object}.", + "snoozed_one": "'{task}' – {object}: egy órára elnémítottam az emlékeztetőket.", + "guide_checklist_one": "egy lépés az ellenőrzőlistán: {steps}", + "guide_none_plain": "Nincsenek mentett útmutatók, dokumentumok vagy alkatrészek ehhez: '{task}' – {object}.", + "none_due_window": "Ebben az időszakban nincs semmi esedékes.", + "none_due_area": "Itt nincs esedékes karbantartás: {area}.", + "unknown_area_name": "Nem ismerek ilyen nevű területet: '{area}'.", + "reading_recorded": "Rögzítettem a leolvasást: {value}{unit} – '{task}' – {object}.", + "reading_not_a_reading": "'{task}': ez a feladat nem rögzít leolvasást.", + "reading_multi": "'{task}': ez a feladat több leolvasást rögzít — add meg őket a karbantartási panelen.", + "reading_unclear": "Nem hallottam számot ebben: '{value}'.", + "note_added": "Hozzáadtam a jegyzetet: '{task}' – {object}.", + "note_too_long": "'{task}': a jegyzetek megteltek — előbb rövidítsd őket a karbantartási panelen.", + "note_empty": "Mi legyen a jegyzetben?", + "turn_user": "Most {user} következik: '{task}' – {object}.", + "turn_you": "Most te következel: '{task}' – {object}.", + "turn_nobody": "Senki sincs hozzárendelve ehhez: '{task}' – {object}.", + "buy_none": "Semmi sem fogy ki — nem kell alkatrészt venni.", + "buy_list": "Kifogyóban lévő alkatrészek, összesen {count}: {items}.", + "buy_list_one": "Egy alkatrész fogyóban van: {items}.", + "buy_item": "{part} (maradt {stock})", + "bought": "Készletre vettem: {qty} × {part} — most {stock} van készleten.", + "batteries_unavailable": "Nincsenek figyelt elemek.", + "batteries_none": "Egyik elem sem gyenge.", + "batteries_low": "Gyenge elemek, összesen {count}: {items}.", + "batteries_low_one": "Egy elem gyenge: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Venni kell: {needs}.", + "undo_none": "Az elmúlt {minutes} percből nincs tőled mit visszavonni.", + "undo_changed": "'{task}' azóta megváltozott, ezért nem nyúlok hozzá.", + "undone_completed": "Visszavonva: '{task}' – {object} már nincs elvégezve.", + "undone_reading": "Visszavonva: a leolvasás törölve: '{task}' – {object}.", + "undone_skipped": "Visszavonva: '{task}' – {object} már nincs kihagyva.", + "undone_postponed": "Visszavonva: '{task}' – {object} újra a korábbi időponton van.", + "undone_snoozed": "Visszavonva: '{task}' – {object} emlékeztetői újra be vannak kapcsolva.", + "undone_note": "Visszavonva: a jegyzet törölve: '{task}' – {object}.", + "undone_bought": "Visszavonva: {qty} × {part} újra levéve a készletről." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/it.json b/custom_components/maintenance_supporter/assist_sentences/responses/it.json index 8bdf624b..700f0b60 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/it.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/it.json @@ -13,7 +13,7 @@ "skipped": "Ho saltato questo ciclo di '{task}' su {object}. Il prossimo è previsto per il {date}.", "skip_disabled": "Saltare è disattivato per '{task}'.", "not_found": "Non ho trovato nessuna attività di manutenzione che corrisponda a '{name}'.", - "ambiguous": "Corrisponde a più attività: {candidates}. Puoi essere un po' più preciso?", + "ambiguous": "Corrisponde a più attività: {candidates}. Di' quale, per esempio «{example}».", "too_early": "Puoi completare '{task}' solo quando si avvicina la scadenza.", "st_overdue": "{days} giorni di ritardo", "st_overdue_one": "un giorno di ritardo", @@ -43,5 +43,49 @@ "st_phase": " — prossimo passo: {phase}", "st_priority": ", priorità alta", "completed_phase": "'{task}' su {object} completato — passo: {phase}. La prossima volta: {next}.", - "guide_phase": "Passo attuale: {phase} (passo {index} di {count})." + "guide_phase": "Passo attuale: {phase} (passo {index} di {count}).", + "part_ambiguous": "Corrisponde a più ricambi: {candidates}. Di' quale, per esempio «{example}».", + "st_no_date": "senza scadenza", + "st_overdue_plain": "in ritardo", + "st_due_soon_plain": "in scadenza", + "months": "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "Questo giro di '{task}' su {object} è stato saltato.", + "snoozed_one": "Promemoria per '{task}' su {object} silenziati per un'ora.", + "guide_checklist_one": "un passaggio della checklist: {steps}", + "guide_none_plain": "Per '{task}' su {object} non ci sono istruzioni, documenti o ricambi salvati.", + "none_due_window": "In quel periodo non scade nulla.", + "none_due_area": "In {area} non serve nessuna manutenzione.", + "unknown_area_name": "Non conosco nessuna area chiamata '{area}'.", + "reading_recorded": "Lettura di {value}{unit} registrata per '{task}' su {object}.", + "reading_not_a_reading": "'{task}' non registra letture.", + "reading_multi": "'{task}' registra più letture: inseriscile nel pannello Manutenzione.", + "reading_unclear": "Non ho capito nessun numero in '{value}'.", + "note_added": "Nota aggiunta a '{task}' su {object}.", + "note_too_long": "Le note di '{task}' sono piene: accorciale prima nel pannello Manutenzione.", + "note_empty": "Cosa deve dire la nota?", + "turn_user": "Tocca a {user}: '{task}' su {object}.", + "turn_you": "Tocca a te: '{task}' su {object}.", + "turn_nobody": "Nessuno è assegnato a '{task}' su {object}.", + "buy_none": "Non sta finendo niente: non serve comprare ricambi.", + "buy_list": "Stanno finendo {count} ricambi: {items}.", + "buy_list_one": "Sta finendo un ricambio: {items}.", + "buy_item": "{part} (ne restano {stock})", + "bought": "Aggiunti {qty} × {part}: ora ce ne sono {stock} in scorta.", + "batteries_unavailable": "Nessuna batteria è monitorata.", + "batteries_none": "Nessuna batteria è scarica.", + "batteries_low": "{count} batterie sono scariche: {items}.", + "batteries_low_one": "Una batteria è scarica: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Da comprare: {needs}.", + "undo_none": "Non c'è niente di tuo da annullare negli ultimi {minutes} minuti.", + "undo_changed": "'{task}' è cambiata nel frattempo, quindi la lascio così.", + "undone_completed": "Annullato: '{task}' su {object} non è più completata.", + "undone_reading": "Annullato: la lettura di '{task}' su {object} è stata rimossa.", + "undone_skipped": "Annullato: '{task}' su {object} non è più saltata.", + "undone_postponed": "Annullato: '{task}' su {object} torna alla data di prima.", + "undone_snoozed": "Annullato: i promemoria per '{task}' su {object} sono di nuovo attivi.", + "undone_note": "Annullato: la nota su '{task}' ({object}) è stata rimossa.", + "undone_bought": "Annullato: {qty} × {part} tolti dalle scorte." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ja.json b/custom_components/maintenance_supporter/assist_sentences/responses/ja.json index 8b4dd523..c7ad6f38 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/ja.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/ja.json @@ -13,7 +13,7 @@ "skipped": "{object}の「{task}」の今回分をスキップしました。次回は{date}が期限です。", "skip_disabled": "'{task}'のスキップは無効になっています。", "not_found": "「{name}」に一致するメンテナンスタスクは見つかりませんでした。", - "ambiguous": "いくつかのタスクが当てはまります:{candidates}。もう少し詳しく言ってください。", + "ambiguous": "いくつかのタスクが当てはまります:{candidates}。どれか言ってください。例:「{example}」。", "too_early": "「{task}」は期限が近づいてからでないと完了にできません。", "st_overdue": "{days}日遅れ", "st_overdue_one": "1日遅れ", @@ -43,5 +43,49 @@ "st_phase": "(次のステップ: {phase})", "st_priority": "、優先度高", "completed_phase": "{object}の「{task}」を完了しました(ステップ: {phase})。次回は: {next}。", - "guide_phase": "現在のステップ: {phase}({count}ステップ中{index}番目)。" + "guide_phase": "現在のステップ: {phase}({count}ステップ中{index}番目)。", + "part_ambiguous": "いくつかの交換部品が当てはまります:{candidates}。どれか言ってください。例:「{example}」。", + "st_no_date": "期限なし", + "st_overdue_plain": "期限切れ", + "st_due_soon_plain": "もうすぐ期限", + "months": "1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月", + "date_format": "{month}{day}日", + "date_format_year": "{year}年{month}{day}日", + "skipped_no_date": "{object}の「{task}」の今回分をスキップしました。", + "snoozed_one": "{object}の「{task}」のリマインダーを1時間、一時停止しました。", + "guide_checklist_one": "チェックリスト1項目:{steps}", + "guide_none_plain": "{object}の「{task}」には、手順も書類も交換部品も保存されていません。", + "none_due_window": "その期間に期限を迎えるものはありません。", + "none_due_area": "{area}で必要なメンテナンスはありません。", + "unknown_area_name": "「{area}」というエリアは見つかりません。", + "reading_recorded": "{object}の「{task}」に{value}{unit}を記録しました。", + "reading_not_a_reading": "「{task}」は測定値を記録するタスクではありません。", + "reading_multi": "「{task}」は複数の測定値を記録します。メンテナンスパネルで入力してください。", + "reading_unclear": "「{value}」から数値を聞き取れませんでした。", + "note_added": "{object}の「{task}」にメモを追加しました。", + "note_too_long": "「{task}」のメモがいっぱいです。先にメンテナンスパネルで短くしてください。", + "note_empty": "メモには何と書きますか?", + "turn_user": "今の担当は{user}です:{object}の「{task}」。", + "turn_you": "今の担当はあなたです:{object}の「{task}」。", + "turn_nobody": "{object}の「{task}」には担当者がいません。", + "buy_none": "残り少ないものはありません。交換部品を買う必要はありません。", + "buy_list": "残り少ない交換部品が{count}件あります:{items}。", + "buy_list_one": "残り少ない交換部品が1件あります:{items}。", + "buy_item": "{part}(残り{stock})", + "bought": "{qty} × {part}を在庫に追加しました。現在の在庫は{stock}です。", + "batteries_unavailable": "監視しているバッテリーはありません。", + "batteries_none": "残量が少ないバッテリーはありません。", + "batteries_low": "残量が少ないバッテリーが{count}個あります:{items}。", + "batteries_low_one": "残量が少ないバッテリーが1個あります:{items}。", + "battery_item": "{device}({type})", + "batteries_buy": " 購入するもの:{needs}。", + "undo_none": "この{minutes}分間に、あなたが取り消せる操作はありません。", + "undo_changed": "「{task}」はその後変更されたので、そのままにしておきます。", + "undone_completed": "取り消しました:{object}の「{task}」は未完了に戻りました。", + "undone_reading": "取り消しました:{object}の「{task}」の測定値を削除しました。", + "undone_skipped": "取り消しました:{object}の「{task}」のスキップを戻しました。", + "undone_postponed": "取り消しました:{object}の「{task}」は元の日付に戻りました。", + "undone_snoozed": "取り消しました:{object}の「{task}」のリマインダーを再開しました。", + "undone_note": "取り消しました:{object}の「{task}」のメモを削除しました。", + "undone_bought": "取り消しました:{qty} × {part}を在庫から戻しました。" } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ko.json b/custom_components/maintenance_supporter/assist_sentences/responses/ko.json index 0252226b..e5382fe0 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/ko.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/ko.json @@ -13,7 +13,7 @@ "skipped": "{object}의 '{task}', 이번 차례는 건너뛰었어요. 다음은 {date}에 예정돼 있어요.", "skip_disabled": "'{task}'의 건너뛰기가 꺼져 있습니다.", "not_found": "'{name}'에 맞는 정비 작업을 찾지 못했어요.", - "ambiguous": "여러 작업이 해당돼요: {candidates}. 조금 더 구체적으로 말씀해 주세요.", + "ambiguous": "여러 작업이 해당돼요: {candidates}. 어떤 건지 말해 주세요. 예: '{example}'.", "too_early": "'{task}' 작업은 예정일이 가까워져야 완료할 수 있어요.", "st_overdue": "{days}일 지연", "st_overdue_one": "하루 지연", @@ -43,5 +43,49 @@ "st_phase": " — 다음 단계: {phase}", "st_priority": ", 높은 우선순위", "completed_phase": "{object}의 '{task}' 완료 — 단계: {phase}. 다음번: {next}.", - "guide_phase": "현재 단계: {phase} ({count}단계 중 {index}단계)." + "guide_phase": "현재 단계: {phase} ({count}단계 중 {index}단계).", + "part_ambiguous": "여러 예비 부품이 해당돼요: {candidates}. 어떤 건지 말해 주세요. 예: '{example}'.", + "st_no_date": "예정일 없음", + "st_overdue_plain": "지연됨", + "st_due_soon_plain": "곧 예정", + "months": "1월,2월,3월,4월,5월,6월,7월,8월,9월,10월,11월,12월", + "date_format": "{month} {day}일", + "date_format_year": "{year}년 {month} {day}일", + "skipped_no_date": "{object}의 '{task}', 이번 차례는 건너뛰었어요.", + "snoozed_one": "{object}의 '{task}' 알림을 한 시간 동안 미뤄뒀어요.", + "guide_checklist_one": "체크리스트 1단계: {steps}", + "guide_none_plain": "{object}의 '{task}'에는 저장된 안내나 문서, 예비 부품이 없어요.", + "none_due_window": "그 기간에는 예정된 게 없어요.", + "none_due_area": "{area}에는 챙길 정비가 없어요.", + "unknown_area_name": "'{area}'라는 공간은 모르겠어요.", + "reading_recorded": "{object}의 '{task}'에 측정값 {value}{unit}, 기록했어요.", + "reading_not_a_reading": "'{task}' 작업은 측정값을 기록하지 않아요.", + "reading_multi": "'{task}' 작업은 측정값을 여러 개 기록해요. 정비 패널에서 입력해 주세요.", + "reading_unclear": "'{value}'에서 숫자를 알아듣지 못했어요.", + "note_added": "{object}의 '{task}'에 메모를 추가했어요.", + "note_too_long": "'{task}' 작업의 메모가 가득 찼어요. 먼저 정비 패널에서 줄여 주세요.", + "note_empty": "메모에 뭐라고 적을까요?", + "turn_user": "지금 차례: {user} — {object}의 '{task}'.", + "turn_you": "지금은 당신 차례예요: {object}의 '{task}'.", + "turn_nobody": "{object}의 '{task}' 작업에는 담당자가 없어요.", + "buy_none": "떨어져 가는 게 없어요. 예비 부품을 살 필요가 없어요.", + "buy_list": "떨어져 가는 예비 부품이 {count}개 있어요: {items}.", + "buy_list_one": "떨어져 가는 예비 부품이 1개 있어요: {items}.", + "buy_item": "{part} ({stock}개 남음)", + "bought": "{qty} × {part}, 재고에 추가했어요. 지금 재고: {stock}.", + "batteries_unavailable": "추적 중인 배터리가 없어요.", + "batteries_none": "약한 배터리는 없어요.", + "batteries_low": "약한 배터리가 {count}개 있어요: {items}.", + "batteries_low_one": "약한 배터리가 1개 있어요: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " 살 것: {needs}.", + "undo_none": "최근 {minutes}분 동안 되돌릴 작업이 없어요.", + "undo_changed": "'{task}' 작업이 그사이에 바뀌어서 그대로 둘게요.", + "undone_completed": "되돌렸어요: {object}의 '{task}', 완료를 취소했어요.", + "undone_reading": "되돌렸어요: {object}의 '{task}' 측정값을 지웠어요.", + "undone_skipped": "되돌렸어요: {object}의 '{task}', 건너뛰기를 취소했어요.", + "undone_postponed": "되돌렸어요: {object}의 '{task}', 원래 날짜로 돌아갔어요.", + "undone_snoozed": "되돌렸어요: {object}의 '{task}' 알림을 다시 켰어요.", + "undone_note": "되돌렸어요: {object}의 '{task}' 메모를 지웠어요.", + "undone_bought": "되돌렸어요: {qty} × {part}, 재고에서 다시 뺐어요." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/nb.json b/custom_components/maintenance_supporter/assist_sentences/responses/nb.json index 78392c34..31379c57 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/nb.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/nb.json @@ -13,7 +13,7 @@ "skipped": "Jeg hoppet over denne runden av '{task}' på {object}. Neste runde skal gjøres {date}.", "skip_disabled": "Hopp over er slått av for '{task}'.", "not_found": "Jeg fant ingen vedlikeholdsoppgave som passer med '{name}'.", - "ambiguous": "Det passer med flere oppgaver: {candidates}. Vær litt mer presis.", + "ambiguous": "Det passer med flere oppgaver: {candidates}. Si hvilken, for eksempel «{example}».", "too_early": "'{task}' kan først krysses av nærmere forfallsdatoen.", "st_overdue": "{days} dager for sent", "st_overdue_one": "én dag for sent", @@ -43,5 +43,49 @@ "st_phase": " — neste trinn: {phase}", "st_priority": ", høy prioritet", "completed_phase": "'{task}' på {object} fullført — trinn: {phase}. Neste gang: {next}.", - "guide_phase": "Gjeldende trinn: {phase} (trinn {index} av {count})." + "guide_phase": "Gjeldende trinn: {phase} (trinn {index} av {count}).", + "part_ambiguous": "Det passer med flere reservedeler: {candidates}. Si hvilken, for eksempel «{example}».", + "st_no_date": "uten forfallsdato", + "st_overdue_plain": "forsinket", + "st_due_soon_plain": "snart", + "months": "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember", + "date_format": "{day}. {month}", + "date_format_year": "{day}. {month} {year}", + "skipped_no_date": "Denne runden av '{task}' på {object} er hoppet over.", + "snoozed_one": "Påminnelser for '{task}' på {object} er satt på pause i én time.", + "guide_checklist_one": "ett sjekklistetrinn: {steps}", + "guide_none_plain": "Det finnes ingen lagrede instruksjoner, dokumenter eller reservedeler for '{task}' på {object}.", + "none_due_window": "Ingenting forfaller i den perioden.", + "none_due_area": "Ingenting trenger vedlikehold i {area}.", + "unknown_area_name": "Jeg kjenner ikke til noe område som heter '{area}'.", + "reading_recorded": "Avlesning {value}{unit} registrert for '{task}' på {object}.", + "reading_not_a_reading": "'{task}' registrerer ingen avlesning.", + "reading_multi": "'{task}' registrerer flere avlesninger — legg dem inn i vedlikeholdspanelet.", + "reading_unclear": "Jeg fikk ikke med meg noe tall i '{value}'.", + "note_added": "Notatet er lagt til '{task}' på {object}.", + "note_too_long": "Notatene for '{task}' er fulle — gjør dem kortere i vedlikeholdspanelet først.", + "note_empty": "Hva skal det stå i notatet?", + "turn_user": "{user} har tur: '{task}' på {object}.", + "turn_you": "Det er din tur: '{task}' på {object}.", + "turn_nobody": "Ingen er tildelt '{task}' på {object}.", + "buy_none": "Ingenting holder på å gå tomt — ingen reservedeler må kjøpes.", + "buy_list": "{count} reservedeler holder på å gå tomme: {items}.", + "buy_list_one": "Én reservedel holder på å gå tom: {items}.", + "buy_item": "{part} ({stock} igjen)", + "bought": "{qty} × {part} lagt på lager — nå {stock} på lager.", + "batteries_unavailable": "Ingen batterier blir overvåket.", + "batteries_none": "Ingen batterier er svake.", + "batteries_low": "{count} batterier er svake: {items}.", + "batteries_low_one": "Ett batteri er svakt: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Må kjøpes: {needs}.", + "undo_none": "Det er ingenting fra deg å angre fra de siste {minutes} minuttene.", + "undo_changed": "'{task}' er endret siden, så jeg lar den være.", + "undone_completed": "Angret: '{task}' på {object} er ikke lenger utført.", + "undone_reading": "Angret: avlesningen for '{task}' på {object} er fjernet.", + "undone_skipped": "Angret: '{task}' på {object} er ikke lenger hoppet over.", + "undone_postponed": "Angret: '{task}' på {object} har den gamle datoen igjen.", + "undone_snoozed": "Angret: påminnelsene for '{task}' på {object} er på igjen.", + "undone_note": "Angret: notatet på '{task}' ({object}) er fjernet.", + "undone_bought": "Angret: {qty} × {part} tatt ut av lageret igjen." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/nl.json b/custom_components/maintenance_supporter/assist_sentences/responses/nl.json index 7c30e945..1fe171b8 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/nl.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/nl.json @@ -13,7 +13,7 @@ "skipped": "Ik heb deze ronde van '{task}' bij {object} overgeslagen. De volgende staat op {date}.", "skip_disabled": "Overslaan is uitgeschakeld voor '{task}'.", "not_found": "Ik kon geen onderhoudstaak vinden die past bij '{name}'.", - "ambiguous": "Dat past bij meerdere taken: {candidates}. Kun je het iets preciezer zeggen?", + "ambiguous": "Dat past bij meerdere taken: {candidates}. Zeg welke je bedoelt, bijvoorbeeld ‘{example}’.", "too_early": "'{task}' kun je pas afvinken als de datum dichterbij is.", "st_overdue": "{days} dagen te laat", "st_overdue_one": "één dag te laat", @@ -43,5 +43,49 @@ "st_phase": " — volgende stap: {phase}", "st_priority": ", hoge prioriteit", "completed_phase": "'{task}' op {object} afgerond — stap: {phase}. Volgende keer: {next}.", - "guide_phase": "Huidige stap: {phase} (stap {index} van {count})." + "guide_phase": "Huidige stap: {phase} (stap {index} van {count}).", + "part_ambiguous": "Dat past bij meerdere onderdelen: {candidates}. Zeg welk je bedoelt, bijvoorbeeld ‘{example}’.", + "st_no_date": "zonder vervaldatum", + "st_overdue_plain": "te laat", + "st_due_soon_plain": "binnenkort", + "months": "januari,februari,maart,april,mei,juni,juli,augustus,september,oktober,november,december", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "Deze ronde van '{task}' bij {object} is overgeslagen.", + "snoozed_one": "Herinneringen voor '{task}' bij {object} staan een uur op stil.", + "guide_checklist_one": "één stap op de checklist: {steps}", + "guide_none_plain": "Voor '{task}' bij {object} zijn geen instructies, documenten of onderdelen opgeslagen.", + "none_due_window": "In die periode staat er niets gepland.", + "none_due_area": "In {area} is geen onderhoud nodig.", + "unknown_area_name": "Ik ken geen ruimte met de naam '{area}'.", + "reading_recorded": "{value}{unit} genoteerd voor '{task}' bij {object}.", + "reading_not_a_reading": "'{task}' houdt geen meterstand bij.", + "reading_multi": "'{task}' houdt meerdere meterstanden bij — vul ze in het onderhoudspaneel in.", + "reading_unclear": "Ik hoorde geen getal in '{value}'.", + "note_added": "Notitie toegevoegd aan '{task}' bij {object}.", + "note_too_long": "De notities van '{task}' zijn vol — maak ze eerst korter in het onderhoudspaneel.", + "note_empty": "Wat moet er in de notitie staan?", + "turn_user": "{user} is aan de beurt: '{task}' bij {object}.", + "turn_you": "Jij bent aan de beurt: '{task}' bij {object}.", + "turn_nobody": "Niemand is toegewezen aan '{task}' bij {object}.", + "buy_none": "Er raakt niets op — er hoeven geen onderdelen gekocht te worden.", + "buy_list": "{count} onderdelen raken op: {items}.", + "buy_list_one": "Eén onderdeel raakt op: {items}.", + "buy_item": "{part} (nog {stock})", + "bought": "{qty} × {part} toegevoegd — nu {stock} op voorraad.", + "batteries_unavailable": "Er worden geen batterijen bijgehouden.", + "batteries_none": "Geen enkele batterij is bijna leeg.", + "batteries_low": "{count} batterijen zijn bijna leeg: {items}.", + "batteries_low_one": "Eén batterij is bijna leeg: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Te kopen: {needs}.", + "undo_none": "Er is van jou niets uit de laatste {minutes} minuten om ongedaan te maken.", + "undo_changed": "'{task}' is intussen veranderd, dus ik laat het zoals het is.", + "undone_completed": "Ongedaan gemaakt: '{task}' bij {object} is niet meer afgerond.", + "undone_reading": "Ongedaan gemaakt: de meterstand voor '{task}' bij {object} is verwijderd.", + "undone_skipped": "Ongedaan gemaakt: '{task}' bij {object} is niet meer overgeslagen.", + "undone_postponed": "Ongedaan gemaakt: '{task}' bij {object} staat weer op de oude datum.", + "undone_snoozed": "Ongedaan gemaakt: de herinneringen voor '{task}' bij {object} staan weer aan.", + "undone_note": "Ongedaan gemaakt: de notitie bij '{task}' ({object}) is verwijderd.", + "undone_bought": "Ongedaan gemaakt: {qty} × {part} weer uit de voorraad gehaald." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pl.json b/custom_components/maintenance_supporter/assist_sentences/responses/pl.json index 13606fba..476c6d95 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/pl.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/pl.json @@ -13,7 +13,7 @@ "skipped": "Ten cykl zadania '{task}' na urządzeniu {object} został pominięty. Następny termin to {date}.", "skip_disabled": "Pomijanie jest wyłączone dla '{task}'.", "not_found": "Nie znalazłem zadania konserwacyjnego pasującego do '{name}'.", - "ambiguous": "To pasuje do kilku zadań: {candidates}. Doprecyzuj, proszę.", + "ambiguous": "To pasuje do kilku zadań: {candidates}. Powiedz, o które chodzi, na przykład „{example}”.", "too_early": "Zadanie '{task}' można wykonać dopiero bliżej terminu.", "st_overdue": "{days} dni po terminie", "st_overdue_one": "jeden dzień po terminie", @@ -43,5 +43,49 @@ "st_phase": " — następny krok: {phase}", "st_priority": ", wysoki priorytet", "completed_phase": "Ukończono '{task}' ({object}) — krok: {phase}. Następnym razem: {next}.", - "guide_phase": "Bieżący krok: {phase} (krok {index} z {count})." + "guide_phase": "Bieżący krok: {phase} (krok {index} z {count}).", + "part_ambiguous": "To pasuje do kilku części: {candidates}. Powiedz, o którą chodzi, na przykład „{example}”.", + "st_no_date": "bez terminu", + "st_overdue_plain": "po terminie", + "st_due_soon_plain": "wkrótce", + "months": "stycznia,lutego,marca,kwietnia,maja,czerwca,lipca,sierpnia,września,października,listopada,grudnia", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "Pominięto ten cykl zadania '{task}' na urządzeniu {object}.", + "snoozed_one": "Przypomnienia dla zadania '{task}' na urządzeniu {object} wyciszone na godzinę.", + "guide_checklist_one": "jeden krok listy kontrolnej: {steps}", + "guide_none_plain": "Dla zadania '{task}' na urządzeniu {object} nie zapisano instrukcji, dokumentów ani części.", + "none_due_window": "W tym okresie nic nie przypada.", + "none_due_area": "W obszarze {area} nie trzeba nic konserwować.", + "unknown_area_name": "Nie znam obszaru o nazwie '{area}'.", + "reading_recorded": "Zapisano odczyt {value}{unit} dla zadania '{task}' na urządzeniu {object}.", + "reading_not_a_reading": "Zadanie '{task}' nie zapisuje odczytów.", + "reading_multi": "Zadanie '{task}' zapisuje kilka odczytów — wpisz je w panelu konserwacji.", + "reading_unclear": "Nie usłyszałem liczby w '{value}'.", + "note_added": "Dodano notatkę do zadania '{task}' na urządzeniu {object}.", + "note_too_long": "Notatki zadania '{task}' są pełne — najpierw skróć je w panelu konserwacji.", + "note_empty": "Co ma być w notatce?", + "turn_user": "Teraz kolej: {user} — '{task}' na urządzeniu {object}.", + "turn_you": "Teraz twoja kolej: '{task}' na urządzeniu {object}.", + "turn_nobody": "Nikt nie jest przypisany do zadania '{task}' na urządzeniu {object}.", + "buy_none": "Nic się nie kończy — nie trzeba kupować części.", + "buy_list": "Kończą się części, łącznie {count}: {items}.", + "buy_list_one": "Kończy się jedna część: {items}.", + "buy_item": "{part} (zostało {stock})", + "bought": "Dodano {qty} × {part} — teraz w magazynie: {stock}.", + "batteries_unavailable": "Żadne baterie nie są monitorowane.", + "batteries_none": "Żadna bateria nie jest słaba.", + "batteries_low": "Słabe baterie, łącznie {count}: {items}.", + "batteries_low_one": "Jedna bateria jest słaba: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Do kupienia: {needs}.", + "undo_none": "Z ostatnich {minutes} minut nie ma nic twojego do cofnięcia.", + "undo_changed": "Zadanie '{task}' zmieniło się w międzyczasie, więc zostawiam je bez zmian.", + "undone_completed": "Cofnięto: zadanie '{task}' na urządzeniu {object} nie jest już wykonane.", + "undone_reading": "Cofnięto: usunięto odczyt zadania '{task}' na urządzeniu {object}.", + "undone_skipped": "Cofnięto: zadanie '{task}' na urządzeniu {object} nie jest już pominięte.", + "undone_postponed": "Cofnięto: zadanie '{task}' na urządzeniu {object} wróciło na poprzedni termin.", + "undone_snoozed": "Cofnięto: przypomnienia dla zadania '{task}' na urządzeniu {object} są znowu włączone.", + "undone_note": "Cofnięto: usunięto notatkę z zadania '{task}' na urządzeniu {object}.", + "undone_bought": "Cofnięto: {qty} × {part} znów zdjęto ze stanu." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json b/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json index 7b4f2df4..ea031215 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json @@ -13,7 +13,7 @@ "skipped": "Pulei este ciclo de '{task}' em {object}. O próximo vence em {date}.", "skip_disabled": "Pular está desativado para '{task}'.", "not_found": "Não encontrei nenhuma tarefa de manutenção que corresponda a '{name}'.", - "ambiguous": "Isso corresponde a várias tarefas: {candidates}. Seja mais específico, por favor.", + "ambiguous": "Isso corresponde a várias tarefas: {candidates}. Diga qual, por exemplo “{example}”.", "too_early": "'{task}' só pode ser concluída mais perto da data de vencimento.", "st_overdue": "{days} dias de atraso", "st_overdue_one": "um dia de atraso", @@ -43,5 +43,49 @@ "st_phase": " — próximo passo: {phase}", "st_priority": ", prioridade alta", "completed_phase": "'{task}' em {object} concluído — passo: {phase}. Na próxima vez: {next}.", - "guide_phase": "Passo atual: {phase} (passo {index} de {count})." + "guide_phase": "Passo atual: {phase} (passo {index} de {count}).", + "part_ambiguous": "Isso corresponde a várias peças: {candidates}. Diga qual, por exemplo “{example}”.", + "st_no_date": "sem data", + "st_overdue_plain": "atrasada", + "st_due_soon_plain": "em breve", + "months": "janeiro,fevereiro,março,abril,maio,junho,julho,agosto,setembro,outubro,novembro,dezembro", + "date_format": "{day} de {month}", + "date_format_year": "{day} de {month} de {year}", + "skipped_no_date": "Este ciclo de '{task}' em {object} foi pulado.", + "snoozed_one": "Lembretes de '{task}' em {object} silenciados por uma hora.", + "guide_checklist_one": "uma etapa do checklist: {steps}", + "guide_none_plain": "Não há instruções, documentos nem peças salvos para '{task}' em {object}.", + "none_due_window": "Nada vence nesse período.", + "none_due_area": "Não há manutenção pendente em {area}.", + "unknown_area_name": "Não conheço nenhuma área chamada '{area}'.", + "reading_recorded": "Leitura de {value}{unit} registrada para '{task}' em {object}.", + "reading_not_a_reading": "'{task}' não registra leituras.", + "reading_multi": "'{task}' registra várias leituras — informe-as no painel de manutenção.", + "reading_unclear": "Não entendi nenhum número em '{value}'.", + "note_added": "Nota adicionada a '{task}' em {object}.", + "note_too_long": "As notas de '{task}' estão cheias — encurte-as primeiro no painel de manutenção.", + "note_empty": "O que a nota deve dizer?", + "turn_user": "É a vez de {user}: '{task}' em {object}.", + "turn_you": "É a sua vez: '{task}' em {object}.", + "turn_nobody": "Ninguém está designado para '{task}' em {object}.", + "buy_none": "Nada está acabando — não é preciso comprar peças.", + "buy_list": "{count} peças estão acabando: {items}.", + "buy_list_one": "Uma peça está acabando: {items}.", + "buy_item": "{part} (restam {stock})", + "bought": "{qty} × {part} adicionados — agora há {stock} em estoque.", + "batteries_unavailable": "Nenhuma bateria está sendo monitorada.", + "batteries_none": "Nenhuma bateria está fraca.", + "batteries_low": "{count} baterias estão fracas: {items}.", + "batteries_low_one": "Uma bateria está fraca: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Para comprar: {needs}.", + "undo_none": "Não há nada seu para desfazer dos últimos {minutes} minutos.", + "undo_changed": "'{task}' mudou desde então, então vou deixar como está.", + "undone_completed": "Desfeito: '{task}' em {object} não está mais concluída.", + "undone_reading": "Desfeito: a leitura de '{task}' em {object} foi removida.", + "undone_skipped": "Desfeito: '{task}' em {object} não está mais pulada.", + "undone_postponed": "Desfeito: '{task}' em {object} voltou para a data anterior.", + "undone_snoozed": "Desfeito: os lembretes de '{task}' em {object} estão ativos de novo.", + "undone_note": "Desfeito: a nota de '{task}' em {object} foi removida.", + "undone_bought": "Desfeito: {qty} × {part} retirados do estoque." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pt.json b/custom_components/maintenance_supporter/assist_sentences/responses/pt.json index 6f5d491f..d4bf7cbe 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/pt.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/pt.json @@ -13,7 +13,7 @@ "skipped": "Saltei este ciclo de '{task}' em {object}. O próximo está previsto para {date}.", "skip_disabled": "Ignorar está desativado para '{task}'.", "not_found": "Não encontrei nenhuma tarefa de manutenção que corresponda a '{name}'.", - "ambiguous": "Isso corresponde a várias tarefas: {candidates}. Podes ser um pouco mais específico?", + "ambiguous": "Isso corresponde a várias tarefas: {candidates}. Diz qual, por exemplo «{example}».", "too_early": "Só podes concluir '{task}' mais perto da data prevista.", "st_overdue": "{days} dias de atraso", "st_overdue_one": "um dia de atraso", @@ -43,5 +43,49 @@ "st_phase": " — próximo passo: {phase}", "st_priority": ", prioridade alta", "completed_phase": "'{task}' em {object} concluído — passo: {phase}. Da próxima vez: {next}.", - "guide_phase": "Passo atual: {phase} (passo {index} de {count})." + "guide_phase": "Passo atual: {phase} (passo {index} de {count}).", + "part_ambiguous": "Isso corresponde a várias peças: {candidates}. Diz qual, por exemplo «{example}».", + "st_no_date": "sem data", + "st_overdue_plain": "em atraso", + "st_due_soon_plain": "em breve", + "months": "janeiro,fevereiro,março,abril,maio,junho,julho,agosto,setembro,outubro,novembro,dezembro", + "date_format": "{day} de {month}", + "date_format_year": "{day} de {month} de {year}", + "skipped_no_date": "Este ciclo de '{task}' em {object} foi saltado.", + "snoozed_one": "Lembretes de '{task}' em {object} silenciados durante uma hora.", + "guide_checklist_one": "um passo da lista: {steps}", + "guide_none_plain": "Não há instruções, documentos nem peças guardados para '{task}' em {object}.", + "none_due_window": "Nada vence nesse período.", + "none_due_area": "Não há manutenção a fazer em {area}.", + "unknown_area_name": "Não conheço nenhuma divisão chamada '{area}'.", + "reading_recorded": "Leitura de {value}{unit} registada para '{task}' em {object}.", + "reading_not_a_reading": "'{task}' não regista leituras.", + "reading_multi": "'{task}' regista várias leituras — introduz-as no painel de manutenção.", + "reading_unclear": "Não percebi nenhum número em '{value}'.", + "note_added": "Nota adicionada a '{task}' em {object}.", + "note_too_long": "As notas de '{task}' estão cheias — encurta-as primeiro no painel de manutenção.", + "note_empty": "O que deve dizer a nota?", + "turn_user": "É a vez de {user}: '{task}' em {object}.", + "turn_you": "É a tua vez: '{task}' em {object}.", + "turn_nobody": "Ninguém está atribuído a '{task}' em {object}.", + "buy_none": "Nada está a acabar — não é preciso comprar peças.", + "buy_list": "Estão a acabar {count} peças: {items}.", + "buy_list_one": "Está a acabar uma peça: {items}.", + "buy_item": "{part} (restam {stock})", + "bought": "Adicionados {qty} × {part} — agora há {stock} em stock.", + "batteries_unavailable": "Nenhuma bateria está a ser acompanhada.", + "batteries_none": "Nenhuma bateria está fraca.", + "batteries_low": "{count} baterias estão fracas: {items}.", + "batteries_low_one": "Uma bateria está fraca: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Para comprar: {needs}.", + "undo_none": "Não há nada teu para anular dos últimos {minutes} minutos.", + "undo_changed": "'{task}' mudou entretanto, por isso deixo como está.", + "undone_completed": "Anulado: '{task}' em {object} já não está concluída.", + "undone_reading": "Anulado: a leitura de '{task}' em {object} foi removida.", + "undone_skipped": "Anulado: '{task}' em {object} já não está saltada.", + "undone_postponed": "Anulado: '{task}' em {object} voltou à data anterior.", + "undone_snoozed": "Anulado: os lembretes de '{task}' em {object} voltaram a estar ativos.", + "undone_note": "Anulado: a nota de '{task}' em {object} foi removida.", + "undone_bought": "Anulado: {qty} × {part} retirados do stock." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ru.json b/custom_components/maintenance_supporter/assist_sentences/responses/ru.json index 6bcedd82..e8630871 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/ru.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/ru.json @@ -13,7 +13,7 @@ "skipped": "Этот цикл задачи '{task}' у устройства {object} пропущен. Следующий срок — {date}.", "skip_disabled": "Пропуск отключён для '{task}'.", "not_found": "Я не нашёл задачу обслуживания, подходящую под '{name}'.", - "ambiguous": "Под это подходит несколько задач: {candidates}. Уточни, пожалуйста.", + "ambiguous": "Под это подходит несколько задач: {candidates}. Скажи, какую именно, например: «{example}».", "too_early": "Задачу '{task}' можно выполнить только ближе к сроку.", "st_overdue": "просрочка {days} дней", "st_overdue_one": "просрочка один день", @@ -43,5 +43,53 @@ "st_phase": " — следующий шаг: {phase}", "st_priority": ", высокий приоритет", "completed_phase": "«{task}» ({object}) выполнено — шаг: {phase}. В следующий раз: {next}.", - "guide_phase": "Текущий шаг: {phase} (шаг {index} из {count})." + "guide_phase": "Текущий шаг: {phase} (шаг {index} из {count}).", + "part_ambiguous": "Под это подходит несколько запчастей: {candidates}. Скажи, какую именно, например: «{example}».", + "st_no_date": "без срока", + "st_overdue_plain": "просрочено", + "st_due_soon_plain": "скоро срок", + "months": "января,февраля,марта,апреля,мая,июня,июля,августа,сентября,октября,ноября,декабря", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year} г.", + "skipped_no_date": "Этот цикл задачи '{task}' у устройства {object} пропущен.", + "snoozed_one": "Напоминания по задаче '{task}' у устройства {object} не будут приходить в течение часа.", + "guide_checklist_one": "один шаг контрольного списка: {steps}", + "guide_none_plain": "Для задачи '{task}' у устройства {object} нет сохранённых инструкций, документов или запчастей.", + "none_due_window": "На этот период ничего не запланировано.", + "none_due_area": "В зоне {area} обслуживание не требуется.", + "unknown_area_name": "Я не знаю зону с названием '{area}'.", + "reading_recorded": "Показание {value}{unit} записано для задачи '{task}' у устройства {object}.", + "reading_not_a_reading": "Задача '{task}' не записывает показания.", + "reading_multi": "Задача '{task}' записывает несколько показаний — введи их в панели обслуживания.", + "reading_unclear": "Я не расслышал число в '{value}'.", + "note_added": "Заметка добавлена к задаче '{task}' у устройства {object}.", + "note_too_long": "Заметки задачи '{task}' заполнены — сначала сократи их в панели обслуживания.", + "note_empty": "Что написать в заметке?", + "turn_user": "Сейчас очередь: {user} — '{task}' у устройства {object}.", + "turn_you": "Сейчас твоя очередь: '{task}' у устройства {object}.", + "turn_nobody": "За задачу '{task}' у устройства {object} никто не отвечает.", + "buy_none": "Ничего не заканчивается — покупать запчасти не нужно.", + "buy_list": "Заканчиваются запчасти, всего {count}: {items}.", + "buy_list_one": "Заканчивается одна запчасть: {items}.", + "buy_item": "{part} (осталось {stock})", + "bought": "Добавлено {qty} × {part} — теперь на складе {stock}.", + "batteries_unavailable": "Батареи не отслеживаются.", + "batteries_none": "Разряженных батарей нет.", + "batteries_low": "Разряжены батареи, всего {count}: {items}.", + "batteries_low_one": "Разряжена одна батарея: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Купить: {needs}.", + "undo_none": "За последние {minutes} минут от тебя нечего отменять.", + "undo_changed": "Задача '{task}' с тех пор изменилась, поэтому я её не трогаю.", + "undone_completed": "Отменено: задача '{task}' у устройства {object} больше не выполнена.", + "undone_reading": "Отменено: показание для задачи '{task}' у устройства {object} удалено.", + "undone_skipped": "Отменено: задача '{task}' у устройства {object} больше не пропущена.", + "undone_postponed": "Отменено: задача '{task}' у устройства {object} снова на прежней дате.", + "undone_snoozed": "Отменено: напоминания по задаче '{task}' у устройства {object} снова включены.", + "undone_note": "Отменено: заметка к задаче '{task}' у устройства {object} удалена.", + "undone_bought": "Отменено: {qty} × {part} снова убрано со склада.", + "st_overdue_few": "просрочка {days} дня", + "st_overdue_one_n": "просрочка {days} день", + "st_due_in_one_n": "срок в течение {days} дня", + "snoozed_one_n": "Напоминания по задаче '{task}' у устройства {object} не будут приходить в течение {hours} часа." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/sv.json b/custom_components/maintenance_supporter/assist_sentences/responses/sv.json index e6aa21f3..3bc855bc 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/sv.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/sv.json @@ -13,7 +13,7 @@ "skipped": "Jag har hoppat över den här omgången av '{task}' på {object}. Nästa omgång ska göras den {date}.", "skip_disabled": "Hoppa över är avstängt för '{task}'.", "not_found": "Jag hittade ingen underhållsuppgift som matchar '{name}'.", - "ambiguous": "Det matchar flera uppgifter: {candidates}. Var lite mer specifik.", + "ambiguous": "Det matchar flera uppgifter: {candidates}. Säg vilken, till exempel ”{example}”.", "too_early": "'{task}' går att bocka av först närmare förfallodatumet.", "st_overdue": "{days} dagar för sent", "st_overdue_one": "en dag för sent", @@ -43,5 +43,49 @@ "st_phase": " — nästa steg: {phase}", "st_priority": ", hög prioritet", "completed_phase": "'{task}' på {object} slutfört — steg: {phase}. Nästa gång: {next}.", - "guide_phase": "Aktuellt steg: {phase} (steg {index} av {count})." + "guide_phase": "Aktuellt steg: {phase} (steg {index} av {count}).", + "part_ambiguous": "Det matchar flera reservdelar: {candidates}. Säg vilken, till exempel ”{example}”.", + "st_no_date": "utan förfallodatum", + "st_overdue_plain": "försenad", + "st_due_soon_plain": "snart", + "months": "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "Den här omgången av '{task}' på {object} hoppades över.", + "snoozed_one": "Påminnelser för '{task}' på {object} är pausade i en timme.", + "guide_checklist_one": "ett checklistesteg: {steps}", + "guide_none_plain": "Det finns inga sparade instruktioner, dokument eller reservdelar för '{task}' på {object}.", + "none_due_window": "Inget förfaller under den perioden.", + "none_due_area": "Inget behöver underhållas i {area}.", + "unknown_area_name": "Jag känner inte till något område som heter '{area}'.", + "reading_recorded": "Avläsning {value}{unit} registrerad för '{task}' på {object}.", + "reading_not_a_reading": "'{task}' registrerar ingen avläsning.", + "reading_multi": "'{task}' registrerar flera avläsningar — fyll i dem i underhållspanelen.", + "reading_unclear": "Jag uppfattade inget tal i '{value}'.", + "note_added": "Anteckningen har lagts till på '{task}' på {object}.", + "note_too_long": "Anteckningarna för '{task}' är fulla — korta ner dem i underhållspanelen först.", + "note_empty": "Vad ska det stå i anteckningen?", + "turn_user": "{user} har turen: '{task}' på {object}.", + "turn_you": "Det är din tur: '{task}' på {object}.", + "turn_nobody": "Ingen är tilldelad '{task}' på {object}.", + "buy_none": "Inget håller på att ta slut — inga reservdelar behöver köpas.", + "buy_list": "{count} reservdelar håller på att ta slut: {items}.", + "buy_list_one": "En reservdel håller på att ta slut: {items}.", + "buy_item": "{part} ({stock} kvar)", + "bought": "{qty} × {part} inlagda i lagret — nu {stock} i lager.", + "batteries_unavailable": "Inga batterier övervakas.", + "batteries_none": "Inga batterier är svaga.", + "batteries_low": "{count} batterier är svaga: {items}.", + "batteries_low_one": "Ett batteri är svagt: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Att köpa: {needs}.", + "undo_none": "Det finns inget från dig att ångra från de senaste {minutes} minuterna.", + "undo_changed": "'{task}' har ändrats sedan dess, så jag låter den vara.", + "undone_completed": "Ångrat: '{task}' på {object} är inte längre utförd.", + "undone_reading": "Ångrat: avläsningen för '{task}' på {object} är borttagen.", + "undone_skipped": "Ångrat: '{task}' på {object} är inte längre överhoppad.", + "undone_postponed": "Ångrat: '{task}' på {object} har sitt gamla datum igen.", + "undone_snoozed": "Ångrat: påminnelserna för '{task}' på {object} är på igen.", + "undone_note": "Ångrat: anteckningen på '{task}' ({object}) är borttagen.", + "undone_bought": "Ångrat: {qty} × {part} har tagits ur lagret igen." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/tr.json b/custom_components/maintenance_supporter/assist_sentences/responses/tr.json index 67ab1a8a..39b39389 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/tr.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/tr.json @@ -13,7 +13,7 @@ "skipped": "{object} için '{task}' görevinin bu turu atlandı. Sıradaki {date} tarihinde.", "skip_disabled": "'{task}' için atlama kapalı.", "not_found": "'{name}' ile eşleşen bir bakım görevi bulamadım.", - "ambiguous": "Birden fazla göreve uyuyor: {candidates}. Biraz daha açık söyler misin?", + "ambiguous": "Birden fazla göreve uyuyor: {candidates}. Hangisi olduğunu söyle, örneğin “{example}”.", "too_early": "'{task}' görevi ancak son tarihine yaklaşınca tamamlanabilir.", "st_overdue": "{days} gün gecikmiş", "st_overdue_one": "bir gün gecikmiş", @@ -43,5 +43,49 @@ "st_phase": " — sonraki adım: {phase}", "st_priority": ", yüksek öncelik", "completed_phase": "{object} üzerindeki '{task}' tamamlandı — adım: {phase}. Bir dahaki sefere: {next}.", - "guide_phase": "Geçerli adım: {phase} ({count} adımdan {index}.)." + "guide_phase": "Geçerli adım: {phase} ({count} adımdan {index}.).", + "part_ambiguous": "Birden fazla yedek parçaya uyuyor: {candidates}. Hangisi olduğunu söyle, örneğin “{example}”.", + "st_no_date": "tarihi yok", + "st_overdue_plain": "gecikmiş", + "st_due_soon_plain": "yakında", + "months": "Ocak,Şubat,Mart,Nisan,Mayıs,Haziran,Temmuz,Ağustos,Eylül,Ekim,Kasım,Aralık", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year}", + "skipped_no_date": "{object} için '{task}' görevinin bu turu atlandı.", + "snoozed_one": "{object} için '{task}' görevinin hatırlatmaları bir saat susturuldu.", + "guide_checklist_one": "bir kontrol listesi adımı: {steps}", + "guide_none_plain": "{object} için '{task}' görevine ait kayıtlı bir talimat, belge ya da yedek parça yok.", + "none_due_window": "O süre içinde yapılacak bir şey yok.", + "none_due_area": "{area} alanında bakım gerekmiyor.", + "unknown_area_name": "'{area}' adında bir alan bilmiyorum.", + "reading_recorded": "{object} için '{task}' görevine {value}{unit} okuması kaydedildi.", + "reading_not_a_reading": "'{task}' görevi okuma kaydetmiyor.", + "reading_multi": "'{task}' görevi birden fazla okuma kaydediyor — bunları bakım panelinden gir.", + "reading_unclear": "'{value}' içinde bir sayı duyamadım.", + "note_added": "{object} için '{task}' görevine not eklendi.", + "note_too_long": "'{task}' görevinin notları dolu — önce bakım panelinden kısalt.", + "note_empty": "Notta ne yazsın?", + "turn_user": "Sıradaki kişi: {user} — {object} için '{task}'.", + "turn_you": "Sıra sende: {object} için '{task}'.", + "turn_nobody": "{object} için '{task}' görevine kimse atanmamış.", + "buy_none": "Tükenmek üzere olan bir şey yok — yedek parça almaya gerek yok.", + "buy_list": "{count} yedek parça tükenmek üzere: {items}.", + "buy_list_one": "Bir yedek parça tükenmek üzere: {items}.", + "buy_item": "{part} ({stock} kaldı)", + "bought": "{qty} × {part} stoğa eklendi — şimdi stokta {stock} var.", + "batteries_unavailable": "Takip edilen pil yok.", + "batteries_none": "Zayıf pil yok.", + "batteries_low": "{count} pil zayıf: {items}.", + "batteries_low_one": "Bir pil zayıf: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Alınacaklar: {needs}.", + "undo_none": "Son {minutes} dakikada senin geri alınacak bir işlemin yok.", + "undo_changed": "'{task}' o zamandan beri değişti, olduğu gibi bırakıyorum.", + "undone_completed": "Geri alındı: {object} için '{task}' artık tamamlanmış değil.", + "undone_reading": "Geri alındı: {object} için '{task}' okuması silindi.", + "undone_skipped": "Geri alındı: {object} için '{task}' artık atlanmış değil.", + "undone_postponed": "Geri alındı: {object} için '{task}' eski tarihine döndü.", + "undone_snoozed": "Geri alındı: {object} için '{task}' hatırlatmaları yeniden açık.", + "undone_note": "Geri alındı: {object} için '{task}' notu silindi.", + "undone_bought": "Geri alındı: {qty} × {part} stoktan yeniden düşüldü." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/uk.json b/custom_components/maintenance_supporter/assist_sentences/responses/uk.json index aefb2af9..269e4a17 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/uk.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/uk.json @@ -13,7 +13,7 @@ "skipped": "Цей цикл завдання '{task}' на пристрої {object} пропущено. Наступний термін — {date}.", "skip_disabled": "Пропуск вимкнено для '{task}'.", "not_found": "Я не знайшов завдання з обслуговування, яке відповідає '{name}'.", - "ambiguous": "Цьому відповідає кілька завдань: {candidates}. Уточни, будь ласка.", + "ambiguous": "Цьому відповідає кілька завдань: {candidates}. Скажи, яке саме, наприклад: «{example}».", "too_early": "Завдання '{task}' можна виконати лише ближче до терміну.", "st_overdue": "прострочення {days} днів", "st_overdue_one": "прострочення один день", @@ -43,5 +43,53 @@ "st_phase": " — наступний крок: {phase}", "st_priority": ", високий пріоритет", "completed_phase": "«{task}» ({object}) виконано — крок: {phase}. Наступного разу: {next}.", - "guide_phase": "Поточний крок: {phase} (крок {index} з {count})." + "guide_phase": "Поточний крок: {phase} (крок {index} з {count}).", + "part_ambiguous": "Цьому відповідає кілька запчастин: {candidates}. Скажи, яку саме, наприклад: «{example}».", + "st_no_date": "без терміну", + "st_overdue_plain": "прострочено", + "st_due_soon_plain": "термін скоро", + "months": "січня,лютого,березня,квітня,травня,червня,липня,серпня,вересня,жовтня,листопада,грудня", + "date_format": "{day} {month}", + "date_format_year": "{day} {month} {year} р.", + "skipped_no_date": "Цей цикл завдання '{task}' на пристрої {object} пропущено.", + "snoozed_one": "Нагадування щодо завдання '{task}' на пристрої {object} не надходитимуть протягом години.", + "guide_checklist_one": "один крок контрольного списку: {steps}", + "guide_none_plain": "Для завдання '{task}' на пристрої {object} немає збережених інструкцій, документів чи запчастин.", + "none_due_window": "На цей період нічого не заплановано.", + "none_due_area": "У зоні {area} обслуговування не потрібне.", + "unknown_area_name": "Я не знаю зони з назвою '{area}'.", + "reading_recorded": "Показник {value}{unit} записано для завдання '{task}' на пристрої {object}.", + "reading_not_a_reading": "Завдання '{task}' не записує показників.", + "reading_multi": "Завдання '{task}' записує кілька показників — введи їх у панелі обслуговування.", + "reading_unclear": "Я не почув числа в '{value}'.", + "note_added": "Нотатку додано до завдання '{task}' на пристрої {object}.", + "note_too_long": "Нотатки завдання '{task}' заповнені — спершу скороти їх у панелі обслуговування.", + "note_empty": "Що написати в нотатці?", + "turn_user": "Зараз черга: {user} — '{task}' на пристрої {object}.", + "turn_you": "Зараз твоя черга: '{task}' на пристрої {object}.", + "turn_nobody": "За завдання '{task}' на пристрої {object} ніхто не відповідає.", + "buy_none": "Нічого не закінчується — купувати запчастини не потрібно.", + "buy_list": "Закінчуються запчастини, усього {count}: {items}.", + "buy_list_one": "Закінчується одна запчастина: {items}.", + "buy_item": "{part} (залишилося {stock})", + "bought": "Додано {qty} × {part} — тепер на складі {stock}.", + "batteries_unavailable": "Батареї не відстежуються.", + "batteries_none": "Розряджених батарей немає.", + "batteries_low": "Розряджені батареї, усього {count}: {items}.", + "batteries_low_one": "Розряджена одна батарея: {items}.", + "battery_item": "{device} ({type})", + "batteries_buy": " Купити: {needs}.", + "undo_none": "За останні {minutes} хвилин від тебе нічого скасовувати.", + "undo_changed": "Завдання '{task}' відтоді змінилося, тож я його не чіпаю.", + "undone_completed": "Скасовано: завдання '{task}' на пристрої {object} більше не виконане.", + "undone_reading": "Скасовано: показник для завдання '{task}' на пристрої {object} видалено.", + "undone_skipped": "Скасовано: завдання '{task}' на пристрої {object} більше не пропущене.", + "undone_postponed": "Скасовано: завдання '{task}' на пристрої {object} знову на попередній даті.", + "undone_snoozed": "Скасовано: нагадування щодо завдання '{task}' на пристрої {object} знову ввімкнені.", + "undone_note": "Скасовано: нотатку до завдання '{task}' на пристрої {object} видалено.", + "undone_bought": "Скасовано: {qty} × {part} знову знято зі складу.", + "st_overdue_few": "прострочення {days} дні", + "st_overdue_one_n": "прострочення {days} день", + "st_due_in_one_n": "термін протягом {days} дня", + "snoozed_one_n": "Нагадування щодо завдання '{task}' на пристрої {object} не надходитимуть протягом {hours} години." } diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/zh.json b/custom_components/maintenance_supporter/assist_sentences/responses/zh.json index fbf3fe53..8afbd6df 100644 --- a/custom_components/maintenance_supporter/assist_sentences/responses/zh.json +++ b/custom_components/maintenance_supporter/assist_sentences/responses/zh.json @@ -13,7 +13,7 @@ "skipped": "已跳过{object}的“{task}”这一次。下一次在{date}到期。", "skip_disabled": "已为'{task}'关闭跳过。", "not_found": "我没有找到和“{name}”对得上的维护任务。", - "ambiguous": "有好几项任务都对得上:{candidates}。请说得更具体一些。", + "ambiguous": "有好几项任务都对得上:{candidates}。请说是哪一项,例如“{example}”。", "too_early": "“{task}”要等到接近到期日才能完成。", "st_overdue": "已逾期{days}天", "st_overdue_one": "已逾期一天", @@ -43,5 +43,49 @@ "st_phase": "——下一步:{phase}", "st_priority": ",高优先级", "completed_phase": "已完成{object}的“{task}”——步骤:{phase}。下次:{next}。", - "guide_phase": "当前步骤:{phase}(第 {index} 步,共 {count} 步)。" + "guide_phase": "当前步骤:{phase}(第 {index} 步,共 {count} 步)。", + "part_ambiguous": "有好几个备件都对得上:{candidates}。请说是哪一个,例如“{example}”。", + "st_no_date": "没有到期日", + "st_overdue_plain": "已逾期", + "st_due_soon_plain": "即将到期", + "months": "1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月", + "date_format": "{month}{day}日", + "date_format_year": "{year}年{month}{day}日", + "skipped_no_date": "已跳过{object}的“{task}”这一次。", + "snoozed_one": "已把{object}的“{task}”提醒暂停1小时。", + "guide_checklist_one": "1个清单步骤:{steps}", + "guide_none_plain": "{object}的“{task}”没有保存任何操作说明、文档或备件。", + "none_due_window": "这段时间内没有到期的任务。", + "none_due_area": "{area}没有需要处理的维护。", + "unknown_area_name": "我不知道有叫“{area}”的区域。", + "reading_recorded": "已为{object}的“{task}”记录读数{value}{unit}。", + "reading_not_a_reading": "“{task}”不记录读数。", + "reading_multi": "“{task}”要记录多个读数,请在维护面板里填写。", + "reading_unclear": "我没从“{value}”里听出数字。", + "note_added": "已给{object}的“{task}”添加备注。", + "note_too_long": "“{task}”的备注已满,请先在维护面板里删减。", + "note_empty": "备注要写什么?", + "turn_user": "现在轮到{user}:{object}的“{task}”。", + "turn_you": "现在轮到你:{object}的“{task}”。", + "turn_nobody": "{object}的“{task}”没有指派任何人。", + "buy_none": "没有快用完的东西,不需要买备件。", + "buy_list": "有{count}个备件快用完了:{items}。", + "buy_list_one": "有1个备件快用完了:{items}。", + "buy_item": "{part}(剩{stock})", + "bought": "已入库{qty} × {part},现在库存{stock}。", + "batteries_unavailable": "没有在监控任何电池。", + "batteries_none": "没有电量低的电池。", + "batteries_low": "有{count}个电池电量低:{items}。", + "batteries_low_one": "有1个电池电量低:{items}。", + "battery_item": "{device}({type})", + "batteries_buy": " 需要购买:{needs}。", + "undo_none": "最近{minutes}分钟内没有你可以撤销的操作。", + "undo_changed": "“{task}”之后已经变了,我就不动它了。", + "undone_completed": "已撤销:{object}的“{task}”不再是已完成。", + "undone_reading": "已撤销:已删除{object}的“{task}”读数。", + "undone_skipped": "已撤销:{object}的“{task}”不再是已跳过。", + "undone_postponed": "已撤销:{object}的“{task}”回到原来的日期。", + "undone_snoozed": "已撤销:{object}的“{task}”提醒已重新开启。", + "undone_note": "已撤销:已删除{object}的“{task}”备注。", + "undone_bought": "已撤销:已把{qty} × {part}从库存中扣回。" } diff --git a/custom_components/maintenance_supporter/binary_sensor.py b/custom_components/maintenance_supporter/binary_sensor.py index 00d58512..3cf44faf 100644 --- a/custom_components/maintenance_supporter/binary_sensor.py +++ b/custom_components/maintenance_supporter/binary_sensor.py @@ -34,6 +34,7 @@ from .const import ( from .coordinator import MaintenanceCoordinator from .entity.entity_base import MaintenanceEntity from .helpers.aggregate import object_slug as aggregate_object_slug +from .helpers.coordinator_data import has_coordinator_data from .helpers.status import compute_status_from_task_dict if TYPE_CHECKING: @@ -148,7 +149,7 @@ class MaintenanceBinarySensor(MaintenanceEntity, BinarySensorEntity): Even if coordinator data is not yet refreshed, clearing _trigger_active and recomputing status here ensures correctness regardless of ordering. """ - if self.coordinator.data is None: + if not has_coordinator_data(self.coordinator): return tasks = self.coordinator.data.get(CONF_TASKS, {}) diff --git a/custom_components/maintenance_supporter/calendar.py b/custom_components/maintenance_supporter/calendar.py index 4c646cee..4e56edc5 100644 --- a/custom_components/maintenance_supporter/calendar.py +++ b/custom_components/maintenance_supporter/calendar.py @@ -591,10 +591,31 @@ class MaintenanceCalendar(CalendarEntity): self._cached_next_event: CalendarEvent | None = None self._cache_time: datetime | None = None + _removed = False + def invalidate_cache(self) -> None: """Invalidate the cached next event (called by coordinators on update).""" self._cache_time = None + @property + def is_live(self) -> bool: + """Added to Home Assistant and not removed since.""" + return self.hass is not None and not self._removed + + async def async_will_remove_from_hass(self) -> None: + """Stop the coordinators repainting a removed calendar. + + Home Assistant's calendar arms an alarm for the end of the current + event on every state write and cancels them here. A coordinator + refresh between a reload's unload and set-up still wrote the old + entity — arming an alarm nobody cancelled any more (a timer left + behind by every reload of the global entry; bug audit 2026-09-29). + """ + self._removed = True + await super().async_will_remove_from_hass() + if self._hass.data.get(DOMAIN, {}).get("_calendar_entity") is self: + self._hass.data[DOMAIN].pop("_calendar_entity", None) + @property def event(self) -> CalendarEvent | None: """Return the next upcoming event (cached for up to 1 hour).""" @@ -697,6 +718,9 @@ class MaintenanceCalendar(CalendarEntity): # Archived task → no calendar entries (inert). if task.archived_at is not None: continue + # #193: a paused task's schedule is frozen, like a paused object's. + if task.paused_at is not None: + continue # Inject live trigger state from coordinator live = live_tasks.get(task_id, {}) diff --git a/custom_components/maintenance_supporter/config_flow.py b/custom_components/maintenance_supporter/config_flow.py index 982d1511..e65bd68b 100644 --- a/custom_components/maintenance_supporter/config_flow.py +++ b/custom_components/maintenance_supporter/config_flow.py @@ -535,6 +535,11 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con part = normalize_part({**praw, "id": pid}) except (PartValidationError, TypeError): continue + # A backup's stock rides along; the entry's first setup moves + # it into the Store (storage.async_migrate_to_store). + stock = praw.get("stock") if isinstance(praw, dict) else None + if isinstance(stock, (int, float)) and not isinstance(stock, bool) and stock >= 0: + part["stock"] = stock parts[part["id"]] = part data: dict[str, Any] = { @@ -704,6 +709,7 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con counter_step=self.async_step_trigger_counter, state_change_step=self.async_step_trigger_state_change, runtime_step=self.async_step_trigger_runtime, + due_date_step=self.async_step_trigger_due_date, compound_step=self.async_step_compound_logic, ) @@ -743,6 +749,15 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con on_complete=self._save_task_and_return, ) + async def async_step_trigger_due_date(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + """Configure due-date trigger.""" + self._on_cancel = lambda: self.async_step_trigger_type() + return await self._trigger_due_date_config( + user_input, + step_id="trigger_due_date", + on_complete=self._save_task_and_return, + ) + # --- Compound Trigger Steps --- async def async_step_compound_logic(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: @@ -776,6 +791,7 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con counter_step=self.async_step_compound_condition_counter, state_change_step=self.async_step_compound_condition_state_change, runtime_step=self.async_step_compound_condition_runtime, + due_date_step=self.async_step_compound_condition_due_date, ) async def async_step_compound_condition_threshold(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: @@ -818,6 +834,16 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con on_complete=self.async_step_compound_review, ) + async def async_step_compound_condition_due_date(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + """Configure due_date for compound condition.""" + self._on_cancel = lambda: self.async_step_compound_condition_type() + return await self._trigger_compound_condition_config( + user_input, + "due_date", + step_id="compound_condition_due_date", + on_complete=self.async_step_compound_review, + ) + async def async_step_compound_review(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Review compound trigger conditions.""" self._on_cancel = lambda: self.async_step_compound_logic() diff --git a/custom_components/maintenance_supporter/config_flow_helpers.py b/custom_components/maintenance_supporter/config_flow_helpers.py index 7e731528..8916aeaf 100644 --- a/custom_components/maintenance_supporter/config_flow_helpers.py +++ b/custom_components/maintenance_supporter/config_flow_helpers.py @@ -260,13 +260,17 @@ def season_ends_schema(current_schedule: dict[str, Any] | None) -> dict[Any, Any count = ends.get("count") until = ends.get("until") + # Suggested, not default: an emptied field is left out of the submission + # and a default re-inserted the stored end, so a series could never be + # made endless again here (audit 2026-09-29). apply_season_ends reads + # absence as "no end". count_key = ( - vol.Optional(CONF_TASK_ENDS_COUNT, default=int(count)) + vol.Optional(CONF_TASK_ENDS_COUNT, description={"suggested_value": int(count)}) if isinstance(count, int) and count >= 1 else vol.Optional(CONF_TASK_ENDS_COUNT) ) until_key = ( - vol.Optional(CONF_TASK_ENDS_UNTIL, default=str(until)) + vol.Optional(CONF_TASK_ENDS_UNTIL, description={"suggested_value": str(until)}) if isinstance(until, str) and until else vol.Optional(CONF_TASK_ENDS_UNTIL) ) diff --git a/custom_components/maintenance_supporter/config_flow_options_global.py b/custom_components/maintenance_supporter/config_flow_options_global.py index b7fd9312..4ec89b55 100644 --- a/custom_components/maintenance_supporter/config_flow_options_global.py +++ b/custom_components/maintenance_supporter/config_flow_options_global.py @@ -29,7 +29,6 @@ from .const import ( CONF_ADVANCED_COMPLETION_ACTIONS, CONF_ADVANCED_ENVIRONMENTAL, CONF_ADVANCED_GROUPS, - CONF_ADVANCED_PREDICTIONS, CONF_ADVANCED_SCHEDULE_TIME, CONF_ADVANCED_SEASONAL, CONF_ARCHIVE_ONEOFF_DAYS, @@ -72,6 +71,7 @@ from .const import ( CONF_PANEL_ENABLED, CONF_PANEL_TITLE, CONF_PART_SEARCH_URL_TEMPLATE, + CONF_PARTS_COST_MODE, CONF_QUIET_HOURS_ENABLED, CONF_QUIET_HOURS_END, CONF_QUIET_HOURS_START, @@ -91,6 +91,7 @@ from .const import ( MAX_PANEL_TITLE_LENGTH, MAX_REMINDER_LEADS, NOTIFY_COMPLETED_MODES, + PARTS_COST_MODES, ROW_ACTION_STYLES, TIME_HHMMSS_PATTERN, ) @@ -742,10 +743,6 @@ class GlobalOptionsFlow(OptionsFlow): CONF_ADVANCED_ADAPTIVE, default=self._opt(CONF_ADVANCED_ADAPTIVE), ): selector.BooleanSelector(), - vol.Optional( - CONF_ADVANCED_PREDICTIONS, - default=self._opt(CONF_ADVANCED_PREDICTIONS), - ): selector.BooleanSelector(), vol.Optional( CONF_ADVANCED_SEASONAL, default=self._opt(CONF_ADVANCED_SEASONAL), @@ -931,6 +928,18 @@ class GlobalOptionsFlow(OptionsFlow): mode=selector.NumberSelectorMode.BOX, ) ), + # #104: spare parts count when bought or when used — every cost + # total follows it, not just the budget. + vol.Optional( + CONF_PARTS_COST_MODE, + default=self._opt(CONF_PARTS_COST_MODE), + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=list(PARTS_COST_MODES), + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="parts_cost_mode", + ) + ), # #146 / #180: the battery fleet's low floor and the level a # low battery must rise above to count as replaced. vol.Optional( diff --git a/custom_components/maintenance_supporter/config_flow_options_task_adaptive.py b/custom_components/maintenance_supporter/config_flow_options_task_adaptive.py index 90a1394d..7cf39664 100644 --- a/custom_components/maintenance_supporter/config_flow_options_task_adaptive.py +++ b/custom_components/maintenance_supporter/config_flow_options_task_adaptive.py @@ -126,7 +126,11 @@ class AdaptiveMixin: """Build the adaptive scheduling form schema.""" env_entity = current_adaptive.get("environmental_entity") env_key = ( - vol.Optional(CONF_ENVIRONMENTAL_ENTITY, default=env_entity) if env_entity else vol.Optional(CONF_ENVIRONMENTAL_ENTITY) + # suggested, not default — a cleared picker is left out of the + # submission and the handler reads absence as "no sensor" + vol.Optional(CONF_ENVIRONMENTAL_ENTITY, description={"suggested_value": env_entity}) + if env_entity + else vol.Optional(CONF_ENVIRONMENTAL_ENTITY) ) return vol.Schema( { diff --git a/custom_components/maintenance_supporter/config_flow_options_task_crud.py b/custom_components/maintenance_supporter/config_flow_options_task_crud.py index 8184130f..1198b1ba 100644 --- a/custom_components/maintenance_supporter/config_flow_options_task_crud.py +++ b/custom_components/maintenance_supporter/config_flow_options_task_crud.py @@ -231,7 +231,17 @@ class TaskCrudMixin: updated_task["schedule"] = {"kind": "interval", **extras} else: updated_task.pop("schedule", None) - # schedule_time only present when global advanced flag is on; clear by submitting "". + # schedule_time only present when global advanced flag is on. The + # form carries the stored time as a SUGGESTED value, and the + # frontend leaves a cleared time field out of the submission — + # absence of an offered field therefore clears (audit 2026-09-29: + # with default= the time could never be removed here). + time_offered = ( + is_schedule_time_enabled(self.hass) + and read_legacy_fields(task)["schedule_type"] in SCHEDULE_TIME_KINDS + ) + if CONF_TASK_SCHEDULE_TIME not in user_input and time_offered: + updated_task.pop("schedule_time", None) if CONF_TASK_SCHEDULE_TIME in user_input: # HA's TimeSelector serialises "HH:MM:SS"; stored form is # the canonical "HH:MM" (helpers.dates.normalize_hhmm). @@ -314,12 +324,12 @@ class TaskCrudMixin: updated_task.pop(CONF_TASK_READING_UNIT, None) # #161 phase 2: reading slots, one "Name | Unit" per line. Ids # are kept for lines whose name already exists (delta chain). - if "readings_text" in user_input: - slots = parse_reading_slots_text(user_input.get("readings_text") or "", task.get("readings")) - if slots: - updated_task["readings"] = slots - else: - updated_task.pop("readings", None) + # Absent = the textarea was emptied (suggested value, see the form). + slots = parse_reading_slots_text(user_input.get("readings_text") or "", task.get("readings")) + if slots: + updated_task["readings"] = slots + else: + updated_task.pop("readings", None) from .helpers.sanitize import cap_task_fields @@ -409,7 +419,10 @@ class TaskCrudMixin: ecd_key = ( vol.Optional("earliest_completion_days") if ecd_stored is None - else vol.Optional("earliest_completion_days", default=ecd_stored) + # suggested, not default: an emptied number field is left out of + # the submission and a default re-inserted the stored window + # (audit 2026-09-29) — the handler reads absence as "no window". + else vol.Optional("earliest_completion_days", description={"suggested_value": ecd_stored}) ) # Prefill the recurrence from whichever storage shape this task uses @@ -479,9 +492,10 @@ class TaskCrudMixin: # while the global advanced flag is on; clear by submitting "". **( { - vol.Optional( - CONF_TASK_SCHEDULE_TIME, - default=task.get("schedule_time", ""), + ( + vol.Optional(CONF_TASK_SCHEDULE_TIME, description={"suggested_value": task["schedule_time"]}) + if task.get("schedule_time") + else vol.Optional(CONF_TASK_SCHEDULE_TIME) ): _OptionalTimeSelector(), } if is_schedule_time_enabled(self.hass) @@ -552,7 +566,7 @@ class TaskCrudMixin: nfc_tag_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)), vol.Optional("require_tag_scan", default=bool(task.get("require_tag_scan"))): selector.BooleanSelector(), reading_unit_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)), - vol.Optional("readings_text", default=reading_slots_text(task.get("readings"))): selector.TextSelector( + _suggested("readings_text", reading_slots_text(task.get("readings"))): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiline=True) ), vol.Optional("go_back", default=False): selector.BooleanSelector(), @@ -596,7 +610,11 @@ class TaskCrudMixin: step_id="edit_checklist", data_schema=vol.Schema( { - vol.Optional("checklist_text", default=default_text): selector.TextSelector( + ( + vol.Optional("checklist_text", description={"suggested_value": default_text}) + if default_text + else vol.Optional("checklist_text") + ): selector.TextSelector( selector.TextSelectorConfig( type=selector.TextSelectorType.TEXT, multiline=True, @@ -663,7 +681,11 @@ class TaskCrudMixin: step_id="edit_phases", data_schema=vol.Schema( { - vol.Optional("phases_text", default=default_text): selector.TextSelector( + ( + vol.Optional("phases_text", description={"suggested_value": default_text}) + if default_text + else vol.Optional("phases_text") + ): selector.TextSelector( selector.TextSelectorConfig( type=selector.TextSelectorType.TEXT, multiline=True, diff --git a/custom_components/maintenance_supporter/config_flow_options_task_trigger.py b/custom_components/maintenance_supporter/config_flow_options_task_trigger.py index 6744c3e0..e64ee736 100644 --- a/custom_components/maintenance_supporter/config_flow_options_task_trigger.py +++ b/custom_components/maintenance_supporter/config_flow_options_task_trigger.py @@ -85,6 +85,8 @@ class TriggerStepsMixin(TriggerConfigMixin): elif ctype == TriggerType.RUNTIME: if cond.get("trigger_runtime_hours") is not None: parts.append(f"hours: {cond['trigger_runtime_hours']}") + elif ctype == TriggerType.DUE_DATE: + parts.append(f"days before: {cond.get('trigger_days_before', 0)}") return parts @staticmethod @@ -348,6 +350,7 @@ class TriggerStepsMixin(TriggerConfigMixin): counter_step=self.async_step_opt_trigger_counter, state_change_step=self.async_step_opt_trigger_state_change, runtime_step=self.async_step_opt_trigger_runtime, + due_date_step=self.async_step_opt_trigger_due_date, compound_step=self.async_step_opt_compound_logic, ) @@ -383,6 +386,14 @@ class TriggerStepsMixin(TriggerConfigMixin): on_complete=self._trigger_on_complete, ) + async def async_step_opt_trigger_due_date(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + """Configure due-date trigger.""" + return await self._trigger_due_date_config( + user_input, + step_id="opt_trigger_due_date", + on_complete=self._trigger_on_complete, + ) + async def async_step_opt_compound_logic(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select compound trigger logic.""" return await self._trigger_compound_logic( @@ -408,6 +419,7 @@ class TriggerStepsMixin(TriggerConfigMixin): counter_step=self.async_step_opt_compound_condition_counter, state_change_step=self.async_step_opt_compound_condition_state_change, runtime_step=self.async_step_opt_compound_condition_runtime, + due_date_step=self.async_step_opt_compound_condition_due_date, ) async def async_step_opt_compound_condition_threshold(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: @@ -446,6 +458,15 @@ class TriggerStepsMixin(TriggerConfigMixin): on_complete=self.async_step_opt_compound_review, ) + async def async_step_opt_compound_condition_due_date(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + """Configure due_date for compound condition.""" + return await self._trigger_compound_condition_config( + user_input, + "due_date", + step_id="compound_condition_due_date", + on_complete=self.async_step_opt_compound_review, + ) + async def async_step_opt_compound_review(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Review compound trigger conditions.""" return await self._trigger_compound_review( @@ -471,4 +492,6 @@ class TriggerStepsMixin(TriggerConfigMixin): async_step_compound_condition_runtime = async_step_opt_compound_condition_runtime + async_step_compound_condition_due_date = async_step_opt_compound_condition_due_date + async_step_compound_review = async_step_opt_compound_review diff --git a/custom_components/maintenance_supporter/config_flow_trigger.py b/custom_components/maintenance_supporter/config_flow_trigger.py index 754c45bb..2d2d48d3 100644 --- a/custom_components/maintenance_supporter/config_flow_trigger.py +++ b/custom_components/maintenance_supporter/config_flow_trigger.py @@ -45,6 +45,7 @@ from .const import ( CONF_TRIGGER_ATTRIBUTE, CONF_TRIGGER_BELOW, CONF_TRIGGER_COMBINATOR, + CONF_TRIGGER_DAYS_BEFORE, CONF_TRIGGER_DELTA_MODE, CONF_TRIGGER_ENTITY, CONF_TRIGGER_ENTITY_LOGIC, @@ -296,6 +297,18 @@ def _runtime_hours_field() -> dict[Any, Any]: } +def _days_before_field() -> dict[Any, Any]: + """due_date: how many days before the reported date the task fires.""" + low, high = TRIGGER_FIELD_RANGES[CONF_TRIGGER_DAYS_BEFORE] + return { + vol.Optional(CONF_TRIGGER_DAYS_BEFORE, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=low, max=high, step=1, mode=selector.NumberSelectorMode.BOX, unit_of_measurement="d" + ) + ) + } + + def _states_from_input(user_input: dict[str, Any]) -> tuple[str, str]: """(from, to) of a state-change submission, trimmed and lowercased — HA states are lowercase, so "ON"/"OFF" must match the state machine.""" @@ -575,6 +588,7 @@ class TriggerConfigMixin: counter_step: Callable[[], Awaitable[ConfigFlowResult]], state_change_step: Callable[[], Awaitable[ConfigFlowResult]], runtime_step: Callable[[], Awaitable[ConfigFlowResult]], + due_date_step: Callable[[], Awaitable[ConfigFlowResult]], compound_step: Callable[[], Awaitable[ConfigFlowResult]] | None = None, ) -> ConfigFlowResult: """Core logic for trigger type selection.""" @@ -592,6 +606,8 @@ class TriggerConfigMixin: return await counter_step() if trigger_type == TriggerType.RUNTIME: return await runtime_step() + if trigger_type == TriggerType.DUE_DATE: + return await due_date_step() if trigger_type == TriggerType.COMPOUND and compound_step: return await compound_step() return await state_change_step() @@ -837,6 +853,34 @@ class TriggerConfigMixin: }, ) + async def _trigger_due_date_config( + self, + user_input: dict[str, Any] | None, + *, + step_id: str, + on_complete: Callable[[], ConfigFlowResult], + ) -> ConfigFlowResult: + """Core logic for due-date trigger configuration.""" + if user_input is not None: + cancel = await self._mixin_check_go_back(user_input) + if cancel is not None: + return cancel + + tc = self._current_task["trigger_config"] + tc[CONF_TRIGGER_DAYS_BEFORE] = int(user_input.get(CONF_TRIGGER_DAYS_BEFORE) or 0) + self._apply_type_step_tail(tc, user_input) + return on_complete() + + state = self._trigger_entity_state + return self.async_show_form( + step_id=step_id, + data_schema=vol.Schema(self._mixin_add_go_back({**_days_before_field(), **self._type_step_tail_fields()})), + description_placeholders={ + "entity_id": self._trigger_entity_id or "", + "current_state": state.state if state is not None else "", + }, + ) + # ------------------------------------------------------------------ # Compound trigger configuration steps # ------------------------------------------------------------------ @@ -944,6 +988,7 @@ class TriggerConfigMixin: counter_step: Callable[[], Awaitable[ConfigFlowResult]], state_change_step: Callable[[], Awaitable[ConfigFlowResult]], runtime_step: Callable[[], Awaitable[ConfigFlowResult]], + due_date_step: Callable[[], Awaitable[ConfigFlowResult]], ) -> ConfigFlowResult: """Select trigger type for a compound condition.""" if user_input is not None: @@ -960,6 +1005,8 @@ class TriggerConfigMixin: return await counter_step() if trigger_type == TriggerType.RUNTIME: return await runtime_step() + if trigger_type == TriggerType.DUE_DATE: + return await due_date_step() return await state_change_step() trigger_options = [t.value for t in TriggerType if t != TriggerType.COMPOUND] @@ -1041,6 +1088,8 @@ class TriggerConfigMixin: states = _parse_states(user_input.get(CONF_TRIGGER_ON_STATES)) if states: cond["trigger_on_states"] = states + elif condition_type == TriggerType.DUE_DATE: + cond["trigger_days_before"] = int(user_input.get(CONF_TRIGGER_DAYS_BEFORE) or 0) entity_ids = cond.get("entity_ids", []) if len(entity_ids) > 1 and user_input.get(CONF_TRIGGER_ENTITY_LOGIC): @@ -1076,6 +1125,8 @@ class TriggerConfigMixin: default=[] if cond.get("entity_id") else "", ): _state_selector(cond.get("entity_id"), multiple=True), } + elif condition_type == TriggerType.DUE_DATE: + schema_fields = _days_before_field() schema_fields.update(_entity_logic_field(cond.get("entity_ids", []))) diff --git a/custom_components/maintenance_supporter/const.py b/custom_components/maintenance_supporter/const.py index f343939d..cf6f9275 100644 --- a/custom_components/maintenance_supporter/const.py +++ b/custom_components/maintenance_supporter/const.py @@ -148,7 +148,9 @@ THRESHOLD_PERCENT_RANGE: tuple[int, int] = (1, 90) # --- Config Keys: Advanced Feature Visibility --- CONF_ADVANCED_ADAPTIVE = "advanced_adaptive_visible" -CONF_ADVANCED_PREDICTIONS = "advanced_predictions_visible" +# advanced_predictions_visible was removed in 2.96 (audit 2026-09-28): it never +# gated anything — sensor predictions are on for every sensor task and switched +# off per task (adaptive_config.sensor_prediction_enabled). Stored values are ignored. CONF_ADVANCED_SEASONAL = "advanced_seasonal_visible" CONF_ADVANCED_ENVIRONMENTAL = "advanced_environmental_visible" CONF_ADVANCED_BUDGET = "advanced_budget_visible" @@ -543,6 +545,7 @@ CONF_TRIGGER_TO_STATE = "trigger_to_state" CONF_TRIGGER_TARGET_CHANGES = "trigger_target_changes" CONF_TRIGGER_RUNTIME_HOURS = "trigger_runtime_hours" CONF_TRIGGER_ON_STATES = "trigger_on_states" +CONF_TRIGGER_DAYS_BEFORE = "trigger_days_before" # --- Config Keys: Adaptive Scheduling --- CONF_ADAPTIVE_CONFIG = "adaptive_config" @@ -599,6 +602,11 @@ CONF_BUDGET_CURRENCY = "budget_currency" # Decimal places for every displayed amount — KPIs, budgets, costs in lists and # reports, budget alerts, buy-task notes. 0 = whole numbers (the default). CONF_CURRENCY_DECIMALS = "currency_decimals" +# #104: when the value of spare parts counts as spending — when they are +# bought (the buy reminder's cost; default, the behaviour so far) or when +# they are used (every completion books the parts it consumed). +CONF_PARTS_COST_MODE = "parts_cost_mode" +PARTS_COST_MODES = ("purchase", "use") DEFAULT_CURRENCY_DECIMALS = 0 BUDGET_CURRENCIES: dict[str, str] = { @@ -757,6 +765,9 @@ class TriggerType(StrEnum): COUNTER = "counter" STATE_CHANGE = "state_change" RUNTIME = "runtime" + # A sensor that REPORTS the date the maintenance is due (a purifier's + # "filter change due" timestamp): fires N days before that date. + DUE_DATE = "due_date" COMPOUND = "compound" @@ -913,6 +924,8 @@ MAX_PART_SEARCH_URL_TEMPLATE_LENGTH = 500 TRIGGER_FIELD_RANGES: dict[str, tuple[int, int]] = { "trigger_for_minutes": (0, 1440), # #136 hold time, one day at most "trigger_target_changes": (1, 10_000), + # due_date: fire this many days before the date the sensor reports. + "trigger_days_before": (0, 365), } # Runtime target hours: > 0 (the WS validator) / >= 1 (the flow selector). TRIGGER_RUNTIME_HOURS_MAX = 100_000 diff --git a/custom_components/maintenance_supporter/coordinator.py b/custom_components/maintenance_supporter/coordinator.py index 55d3c9a6..272899b4 100644 --- a/custom_components/maintenance_supporter/coordinator.py +++ b/custom_components/maintenance_supporter/coordinator.py @@ -39,6 +39,7 @@ from .const import ( EVENT_TASK_SKIPPED, LIFECYCLE_HISTORY_TYPES, MANUAL_COMPLETION_DEDUP_SECONDS, + MAX_INTERVAL_DAYS, MISSING_ENTITY_THRESHOLD_REFRESHES, NOTIFIABLE_STATUSES, NOTIFICATION_MANAGER_KEY, @@ -227,6 +228,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): obj_data = self.entry.data.get(CONF_OBJECT, {}) today = dt_util.now().date() if not pause_due_for_auto_resume(obj_data, today): + await self._async_maybe_auto_resume_tasks(today) return new_data = build_resumed_entry_data(dict(self.entry.data), self._store, today.isoformat()) @@ -241,6 +243,23 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): obj_data.get("name"), ) + async def _async_maybe_auto_resume_tasks(self, today: date) -> None: + """#193: end single-task pauses whose ``paused_until`` day has come — + the object auto-resume's rule, per task (fresh cycle for recurring).""" + from .helpers.pause import pause_due_for_auto_resume, resume_task_data + + tasks = dict(self.entry.data.get(CONF_TASKS) or {}) + due = [tid for tid, td in tasks.items() if isinstance(td, dict) and pause_due_for_auto_resume(td, today)] + if not due: + return + for tid in due: + tasks[tid] = resume_task_data(tid, tasks[tid], self._store, today.isoformat()) + self.hass.config_entries.async_update_entry(self.entry, data={**self.entry.data, CONF_TASKS: tasks}) + await self._store.async_save() + # A paused sensor task set up no triggers — only a reload wires them. + self.hass.config_entries.async_schedule_reload(self.entry.entry_id) + _LOGGER.info("Pause of %d task(s) on '%s' ended (paused_until reached) — resumed", len(due), self.maintenance_object.name) + async def _async_prefetch_calendar_occurrences(self) -> None: """#187: warm the shared calendar-entity event cache BEFORE statuses. @@ -365,7 +384,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): # Same short-circuit surface as archived, but the object remains a # first-class citizen in every view. `_paused` mirrors the status # for the dict-twin recomputation in helpers.status. - if object_paused: + if object_paused or task.paused_at is not None: result[CONF_TASKS][task_id] = _inert_task_result(task, MaintenanceStatus.PAUSED, _paused=True) continue @@ -413,7 +432,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self._async_check_budget(result[CONF_TASKS]) # Notify calendar entity if registered and added to hass - if self._calendar_entity is not None and self._calendar_entity.hass is not None: + if self._calendar_entity is not None and self._calendar_entity.is_live: self._calendar_entity.invalidate_cache() self._calendar_entity.async_write_ha_state() @@ -660,6 +679,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): return from .helpers.trigger_fallback import ( evaluate_counter, + evaluate_due_date, evaluate_runtime, evaluate_state_change, evaluate_threshold, @@ -675,6 +695,8 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): result = evaluate_state_change(task.trigger_config, entity_ids) elif trigger_type == "runtime": result = evaluate_runtime(task.trigger_config, entity_ids) + elif trigger_type == "due_date": + result = evaluate_due_date(self.hass.states.get, task.trigger_config, entity_ids) else: return @@ -1393,6 +1415,12 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): from .helpers.reference_numbers import next_history_ref task.history[-1]["ref_no"] = next_history_ref(self._store, task_id, task.history) + # #104: what the parts of this completion cost at this moment, + # whether it was a purchase, and how it is booked — decided now, + # so a later change of the setting never re-values it. + from .helpers.parts_cost import annotate_completion + + annotate_completion(self.hass, self.entry, merged[task_id], task.history[-1]) # #73: a completed cycle retires its in-cycle checklist ticks — the # snapshot that matters is in the history entry above. A pure backfill # closed no current cycle, so the live ticks stay. @@ -1482,6 +1510,15 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): except Exception: _LOGGER.exception("Part consumption failed for task %s", task_id) + # #104 "when used" bookkeeping: a purchase is stock, not spending — + # what was actually paid becomes the part's price, so the parts it + # bought are valued at that price when they are used. + if is_latest and cost is not None and cost > 0: + from .helpers.parts_cost import COST_BASIS_USE, parts_cost_mode + + if parts_cost_mode(self.hass) == COST_BASIS_USE: + self._record_purchase_price(merged[task_id], float(cost), restock_quantity) + # A status configured to notify ONCE (interval 0) stays silenced by the # _SENT_ONCE sentinel until something clears it; nothing did after a # completion, so a sensor task that re-triggered weeks later was never @@ -1513,6 +1550,10 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): # automation can forward a meter value without reading history. reading_value=reading_value, reading_values=reading_values, + # #104: the value of the parts this completion used (None + # when none carried a price) — the same figure the history + # entry records. + parts_cost=(task.history[-1].get("parts_cost") if task.history else None), # #133: the history entry's own timestamp — identical to what # the history records, so automations can attribute backdated # completions to the right period instead of time_fired. @@ -1625,6 +1666,11 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._lifecycle_event_payload( task, task_id, + # `date` is what the logbook and the docs read — the event + # only carried `reset_date`, so every reset showed as "reset + # to ?" since 2.19 (audit 2026-09-29). `reset_date` stays for + # automations written against the old payload. + date=task.last_performed, reset_date=task.last_performed, ), ) @@ -1638,6 +1684,16 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): _LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title) return self._require_active(task_id, merged[task_id], translation_key="task_inactive_postpone") + # At most one maximum interval out, like the panel — enforced HERE so + # voice ("postpone by 900 days" is one sentence away) cannot write the + # override the WS pre-check refuses (voice audit 2026-09-30). Beyond + # it the calendar's "next day" arithmetic overflowed (audit 2026-09-27). + if until > dt_util.now().date() + timedelta(days=MAX_INTERVAL_DAYS): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="postpone_too_far", + translation_placeholders={"days": str(MAX_INTERVAL_DAYS)}, + ) task = MaintenanceTask.from_dict(merged[task_id]) task.due_override = until.isoformat() # Only the due date moves: the sensor triggers keep their progress @@ -1645,6 +1701,40 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self._persist_and_signal_task_change(task_id, task, occurrence_done=False) _LOGGER.debug("Occurrence postponed to %s: %s on %s", until, task.name, self.maintenance_object.name) + async def async_restore_task_fields( + self, + task_id: str, + dynamic: dict[str, Any], + static: dict[str, Any], + ) -> None: + """Put a task back the way it was before one voice action (undo). + + ``dynamic`` maps Store fields to their earlier values (``None`` = the + field was absent); ``static`` does the same for the entry-level + fields a voice action writes (the rotation pointer, the notes). + Everything else is left as it is now — see ``helpers.voice_undo`` + for which fields those are and why. + """ + if task_id not in (self.entry.data.get(CONF_TASKS) or {}): + return + self._store.update_task_state(task_id, **dynamic) + current = dict(self.entry.data[CONF_TASKS][task_id]) + if any(current.get(key) != value for key, value in static.items()): + for key, value in static.items(): + if value is None: + current.pop(key, None) + else: + current[key] = value + write_task(self.hass, self.entry, task_id, current) + # The undone completion must not swallow the real one that follows it + # as a double tap, nor hold the triggers in their post-completion + # cooldown. + self._recent_manual_completions.pop(task_id, None) + self._recently_completed.pop(task_id, None) + self._completion_cooldown.discard(task_id) + await self._store.async_save() + await self.async_refresh_now() + def _is_inert(self, task_data: dict[str, Any]) -> bool: """Archived / disabled task, or a paused object (helpers.pause.is_task_inert).""" return is_task_inert(task_data, self.entry.data.get(CONF_OBJECT, {})) @@ -1729,6 +1819,24 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) + def _record_purchase_price(self, task_data: dict[str, Any], cost: float, packages: float | None) -> None: + """A buy reminder's paid cost becomes its part's price (per package + with a package size, else per unit) — "when used" mode only.""" + from .const import CONF_PARTS + from .helpers.parts import MAX_PART_COST, PART_REF_FIELD + from .helpers.parts_cost import purchase_unit_price + + ref = task_data.get(PART_REF_FIELD) + parts = dict(self.entry.data.get(CONF_PARTS) or {}) + part = parts.get(ref.get("part_id")) if isinstance(ref, dict) else None + if not isinstance(part, dict): + return + price = purchase_unit_price(part, cost, packages) + if price is None or price > MAX_PART_COST or price == part.get("cost"): + return + parts[part["id"]] = {**part, "cost": price} + self.hass.config_entries.async_update_entry(self.entry, data={**self.entry.data, CONF_PARTS: parts}) + async def _async_notify_completed(self, task_id: str, label: str, source: str | None, completed_by: str | None, completed_at: str) -> None: from .helpers.notification_manager import NotificationManager diff --git a/custom_components/maintenance_supporter/entity/entity_base.py b/custom_components/maintenance_supporter/entity/entity_base.py index bd092649..06caf37e 100644 --- a/custom_components/maintenance_supporter/entity/entity_base.py +++ b/custom_components/maintenance_supporter/entity/entity_base.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any +from homeassistant.helpers import area_registry as ar from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,6 +12,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from ..const import CONF_OBJECT, DOMAIN from ..coordinator import MaintenanceCoordinator from ..helpers import device_link +from ..helpers.coordinator_data import has_coordinator_data class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]): @@ -106,15 +108,19 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]): device_info["model"] = obj["model"] if obj.get("serial_number"): device_info["serial_number"] = obj["serial_number"] - if obj.get("area_id"): - device_info["suggested_area"] = obj["area_id"] + # suggested_area is looked up by NAME: the area id passed here made + # Home Assistant create a second area called e.g. "living_room" next + # to "Living Room" and put the device in it (bug audit 2026-09-29). + # An id no area has any more suggests nothing. + if isinstance(obj.get("area_id"), str) and (area := ar.async_get(self.hass).async_get_area(obj["area_id"])): + device_info["suggested_area"] = area.name return device_info @property def _task_data(self) -> dict[str, Any]: """Return the current task data from coordinator.""" - if self.coordinator.data is None: + if not has_coordinator_data(self.coordinator): return {} tasks: dict[str, Any] = self.coordinator.data.get("tasks", {}) result: dict[str, Any] = tasks.get(self._task_id, {}) diff --git a/custom_components/maintenance_supporter/entity/triggers/__init__.py b/custom_components/maintenance_supporter/entity/triggers/__init__.py index 24b09e99..bda183fa 100644 --- a/custom_components/maintenance_supporter/entity/triggers/__init__.py +++ b/custom_components/maintenance_supporter/entity/triggers/__init__.py @@ -8,6 +8,7 @@ from ...const import TriggerType from .base_trigger import BaseTrigger from .compound import CompoundTrigger from .counter import CounterTrigger +from .due_date import DueDateTrigger from .runtime import RuntimeTrigger from .state_change import StateChangeTrigger from .threshold import ThresholdTrigger @@ -15,7 +16,7 @@ from .threshold import ThresholdTrigger if TYPE_CHECKING: from homeassistant.core import HomeAssistant - from ...sensor import MaintenanceSensor + from .base_trigger import TriggerHost def normalize_entity_ids(trigger_config: dict[str, Any]) -> list[str]: @@ -137,7 +138,7 @@ def _inject_per_entity_state(config: dict[str, Any], entity_state: dict[str, Any def create_trigger( hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> BaseTrigger: """Create a trigger instance based on trigger type.""" @@ -151,6 +152,8 @@ def create_trigger( return StateChangeTrigger(hass, entity, trigger_config) if trigger_type == TriggerType.RUNTIME: return RuntimeTrigger(hass, entity, trigger_config) + if trigger_type == TriggerType.DUE_DATE: + return DueDateTrigger(hass, entity, trigger_config) if trigger_type == TriggerType.COMPOUND: return CompoundTrigger(hass, entity, trigger_config) @@ -159,7 +162,7 @@ def create_trigger( def create_triggers( hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> list[BaseTrigger]: """Create trigger instances for all entity_ids in the config. @@ -206,6 +209,7 @@ __all__ = [ "BaseTrigger", "CompoundTrigger", "CounterTrigger", + "DueDateTrigger", "RuntimeTrigger", "StateChangeTrigger", "ThresholdTrigger", diff --git a/custom_components/maintenance_supporter/entity/triggers/base_trigger.py b/custom_components/maintenance_supporter/entity/triggers/base_trigger.py index 7b9b10a2..690a3125 100644 --- a/custom_components/maintenance_supporter/entity/triggers/base_trigger.py +++ b/custom_components/maintenance_supporter/entity/triggers/base_trigger.py @@ -7,7 +7,7 @@ import math from abc import ABC, abstractmethod from collections.abc import Coroutine from datetime import datetime -from typing import TYPE_CHECKING, Any +from typing import Any, Protocol from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback from homeassistant.helpers.event import ( @@ -15,10 +15,6 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) -if TYPE_CHECKING: - from ...coordinator import MaintenanceCoordinator - from ...sensor import MaintenanceSensor - from ...const import ( EVENT_TRIGGER_ACTIVATED, EVENT_TRIGGER_DEACTIVATED, @@ -28,6 +24,49 @@ from ...helpers.managed_timer import ManagedTimer _LOGGER = logging.getLogger(__name__) + +class TriggerCoordinator(Protocol): + """What a trigger needs from its coordinator: the object's, or a compound + condition's proxy that routes these per condition.""" + + async def async_add_trigger_history_entry(self, task_id: str, trigger_value: float | None = None) -> None: ... + + async def async_auto_complete_on_recovery(self, task_id: str, trigger_value: float) -> None: ... + + async def async_persist_trigger_runtime( + self, + task_id: str, + runtime_data: dict[str, Any], + entity_id: str | None = None, + *, + immediate: bool = False, + ) -> None: ... + + async def async_request_refresh(self) -> None: ... + + def note_trigger_edge(self, task_id: str, *, recovered: bool = True) -> None: ... + + +class TriggerHost(Protocol): + """What a trigger needs from the entity it drives: the task's sensor, or + a compound condition's proxy entity.""" + + @property + def entity_id(self) -> str: ... + + @property + def _task_id(self) -> str: ... + + @property + def coordinator(self) -> TriggerCoordinator: ... + + def async_update_trigger_state( + self, + is_triggered: bool, + current_value: float | None = None, + trigger_entity_id: str | None = None, + ) -> bool | None: ... + # The initial-evaluation retry: an entity that is unknown/unavailable at # setup is re-checked this often, this many times, before the state-change # listener alone is trusted to catch its recovery (issue #1 family). @@ -45,7 +84,7 @@ class BaseTrigger(ABC): def __init__( self, hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> None: """Initialize the trigger.""" @@ -75,7 +114,7 @@ class BaseTrigger(ABC): self._logged_unavailable = False # Log-once pattern for unavailable @property - def _coordinator(self) -> MaintenanceCoordinator: + def _coordinator(self) -> TriggerCoordinator: """Get the coordinator from the entity.""" return self.entity.coordinator @@ -84,6 +123,14 @@ class BaseTrigger(ABC): """Get the task ID from the entity.""" return self.entity._task_id + def _event_ids(self) -> dict[str, Any]: + """The task's ids for the trigger events — EXAMPLES.md tells + automations to deep-link with them (`?entry_id=…&task_id=…`), but + only the completed / skipped / reset events carried them (audit + 2026-09-28).""" + entry = getattr(self._coordinator, "entry", None) + return {"entry_id": getattr(entry, "entry_id", None), "task_id": self._task_id} + async def async_setup(self) -> None: """Set up the trigger: validate entity and register listener. @@ -307,6 +354,12 @@ class BaseTrigger(ABC): """ + @property + def _is_compound_condition(self) -> bool: + """This trigger watches one condition of a compound (its entity is + the condition's proxy), not the task itself.""" + return getattr(self.entity, "is_compound_condition", False) is True + def _request_coordinator_refresh(self) -> None: """#175: a trigger flip must reach the coordinator now, not on the next 5-minute tick. Notifications (and the ``maintenance_supporter_ @@ -332,6 +385,12 @@ class BaseTrigger(ABC): current_value=value, trigger_entity_id=self.entity_id, ) + if self._is_compound_condition: + # One condition of a compound: the compound decides whether the + # TASK activated and announces it. Firing here sent + # maintenance_supporter_trigger_activated with the task's ids when + # one half of an AND flipped (bug audit 2026-09-29). + return # Add history entry for the trigger activation self._track(self._coordinator.async_add_trigger_history_entry(self._task_id, trigger_value=value)) @@ -343,6 +402,7 @@ class BaseTrigger(ABC): EVENT_TRIGGER_ACTIVATED, { "entity_id": self.entity.entity_id, + **self._event_ids(), "trigger_entity": self.entity_id, "trigger_attribute": self.attribute, "trigger_value": value, @@ -367,6 +427,8 @@ class BaseTrigger(ABC): current_value=value, trigger_entity_id=self.entity_id, ) + if self._is_compound_condition: + return # the compound announces the task's recovery (see activation) self._request_coordinator_refresh() @@ -375,6 +437,7 @@ class BaseTrigger(ABC): EVENT_TRIGGER_DEACTIVATED, { "entity_id": self.entity.entity_id, + **self._event_ids(), "trigger_entity": self.entity_id, "trigger_attribute": self.attribute, "trigger_value": value, diff --git a/custom_components/maintenance_supporter/entity/triggers/compound.py b/custom_components/maintenance_supporter/entity/triggers/compound.py index 8e8acffb..452de4ee 100644 --- a/custom_components/maintenance_supporter/entity/triggers/compound.py +++ b/custom_components/maintenance_supporter/entity/triggers/compound.py @@ -8,8 +8,7 @@ from typing import TYPE_CHECKING, Any from homeassistant.core import HomeAssistant, callback if TYPE_CHECKING: - from ...coordinator import MaintenanceCoordinator - from ...sensor import MaintenanceSensor + from .base_trigger import TriggerCoordinator, TriggerHost from ...const import ( CONF_COMPOUND_CONDITIONS, @@ -32,6 +31,11 @@ class CompoundSubEntity: ``CompoundTrigger`` of the condition-level result. """ + # A condition's sub-trigger only reports to this proxy: the task-level + # side effects (the trigger events, the cooldown edge, the refresh) belong + # to the compound — BaseTrigger checks this flag. + is_compound_condition = True + def __init__( self, parent: CompoundTrigger, @@ -52,9 +56,10 @@ class CompoundSubEntity: self._per_entity_values: dict[str, float | None] = {} self._entity_logic = condition_config.get("entity_logic", "any") # Mirror attributes the real entity exposes for trigger access - self.entity_id = parent.entity.entity_id - self._task_id = parent.entity._task_id - self.coordinator = parent.entity.coordinator + self.entity_id: str = parent.entity.entity_id + self._task_id: str = parent.entity._task_id + # Replaced by the condition's _CompoundCoordinatorProxy right after. + self.coordinator: TriggerCoordinator = parent.entity.coordinator @callback def async_update_trigger_state( @@ -91,7 +96,7 @@ class _CompoundCoordinatorProxy: stores data under ``_trigger_state.conditions[idx][entity_id]``. """ - def __init__(self, real_coordinator: MaintenanceCoordinator, condition_idx: int) -> None: + def __init__(self, real_coordinator: TriggerCoordinator, condition_idx: int) -> None: """Initialize the proxy.""" self._real = real_coordinator self._condition_idx = condition_idx @@ -110,19 +115,14 @@ class _CompoundCoordinatorProxy: ) -> None: """Persist under trigger_runtime as a per-condition compound key. - The real coordinator always has a Store; merge_task_data reshapes these - ``_compound_[_]`` keys back into - ``_trigger_state["conditions"][idx]`` on read. + The coordinator stores under whatever key it is given; + merge_task_data reshapes these ``_compound_[_]`` keys + back into ``_trigger_state["conditions"][idx]`` on read. """ - store = self._real._store compound_key = f"_compound_{self._condition_idx}" if entity_id is not None: compound_key = f"_compound_{self._condition_idx}_{entity_id}" - store.set_trigger_runtime(task_id, compound_key, runtime_data) - if immediate: - await store.async_save() - else: - store.async_delay_save() + await self._real.async_persist_trigger_runtime(task_id, runtime_data, compound_key, immediate=immediate) # A condition's sub-trigger is not the task's trigger: only the compound # decides when the TASK activates. Delegated to the real coordinator, @@ -137,6 +137,14 @@ class _CompoundCoordinatorProxy: def note_trigger_edge(self, task_id: str, *, recovered: bool = True) -> None: """No-op for a condition — the compound reports its own edge.""" + async def async_request_refresh(self) -> None: + """Delegate to the object's coordinator.""" + await self._real.async_request_refresh() + + async def async_auto_complete_on_recovery(self, task_id: str, trigger_value: float) -> None: + """Delegate to the object's coordinator.""" + await self._real.async_auto_complete_on_recovery(task_id, trigger_value) + class CompoundTrigger(BaseTrigger): """Compound trigger that combines multiple conditions with AND/OR logic. @@ -154,7 +162,7 @@ class CompoundTrigger(BaseTrigger): def __init__( self, hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> None: """Initialize the compound trigger.""" @@ -222,9 +230,9 @@ class CompoundTrigger(BaseTrigger): # Wrap the real coordinator with a proxy for persistence proxy_coordinator = _CompoundCoordinatorProxy(self._coordinator, idx) - sub_entity.coordinator = proxy_coordinator # type: ignore[assignment] + sub_entity.coordinator = proxy_coordinator - sub_triggers = create_triggers(self.hass, sub_entity, cond_config) # type: ignore[arg-type] + sub_triggers = create_triggers(self.hass, sub_entity, cond_config) self._sub_triggers.append(sub_triggers) for trigger in sub_triggers: @@ -326,6 +334,7 @@ class CompoundTrigger(BaseTrigger): EVENT_TRIGGER_ACTIVATED, { "entity_id": self.entity.entity_id, + **self._event_ids(), "trigger_type": "compound", "compound_logic": self._compound_logic, "condition_states": list(self._condition_states), @@ -350,6 +359,7 @@ class CompoundTrigger(BaseTrigger): EVENT_TRIGGER_DEACTIVATED, { "entity_id": self.entity.entity_id, + **self._event_ids(), "trigger_type": "compound", "compound_logic": self._compound_logic, "condition_states": list(self._condition_states), diff --git a/custom_components/maintenance_supporter/entity/triggers/counter.py b/custom_components/maintenance_supporter/entity/triggers/counter.py index 737389e0..037be2fa 100644 --- a/custom_components/maintenance_supporter/entity/triggers/counter.py +++ b/custom_components/maintenance_supporter/entity/triggers/counter.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any from homeassistant.core import HomeAssistant if TYPE_CHECKING: - from ...sensor import MaintenanceSensor + from .base_trigger import TriggerHost from ...helpers.trigger_fallback import counter_baseline from .base_trigger import BaseTrigger @@ -27,7 +27,7 @@ class CounterTrigger(BaseTrigger): def __init__( self, hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> None: """Initialize counter trigger.""" diff --git a/custom_components/maintenance_supporter/entity/triggers/runtime.py b/custom_components/maintenance_supporter/entity/triggers/runtime.py index 35d1464e..64b235e9 100644 --- a/custom_components/maintenance_supporter/entity/triggers/runtime.py +++ b/custom_components/maintenance_supporter/entity/triggers/runtime.py @@ -27,7 +27,7 @@ from ...const import UNAVAILABLE_STATES from ...helpers.managed_timer import ManagedTimer if TYPE_CHECKING: - from ...sensor import MaintenanceSensor + from .base_trigger import TriggerHost from homeassistant.util import dt as dt_util from .base_trigger import BaseTrigger @@ -50,7 +50,7 @@ class RuntimeTrigger(BaseTrigger): def __init__( self, hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> None: """Initialize runtime trigger.""" diff --git a/custom_components/maintenance_supporter/entity/triggers/state_change.py b/custom_components/maintenance_supporter/entity/triggers/state_change.py index 89ab0a61..1b5d26ff 100644 --- a/custom_components/maintenance_supporter/entity/triggers/state_change.py +++ b/custom_components/maintenance_supporter/entity/triggers/state_change.py @@ -17,7 +17,7 @@ from ...const import UNAVAILABLE_STATES from ...helpers.managed_timer import ManagedTimer if TYPE_CHECKING: - from ...sensor import MaintenanceSensor + from .base_trigger import TriggerHost from .base_trigger import BaseTrigger @@ -48,7 +48,7 @@ class StateChangeTrigger(BaseTrigger): def __init__( self, hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> None: """Initialize state change trigger.""" diff --git a/custom_components/maintenance_supporter/entity/triggers/threshold.py b/custom_components/maintenance_supporter/entity/triggers/threshold.py index 2cda1c66..66af26ba 100644 --- a/custom_components/maintenance_supporter/entity/triggers/threshold.py +++ b/custom_components/maintenance_supporter/entity/triggers/threshold.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util if TYPE_CHECKING: - from ...sensor import MaintenanceSensor + from .base_trigger import TriggerHost from ...helpers.managed_timer import ManagedTimer from ...helpers.trigger_fallback import threshold_exceeds @@ -32,7 +32,7 @@ class ThresholdTrigger(BaseTrigger): def __init__( self, hass: HomeAssistant, - entity: MaintenanceSensor, + entity: TriggerHost, trigger_config: dict[str, Any], ) -> None: """Initialize threshold trigger.""" diff --git a/custom_components/maintenance_supporter/export.py b/custom_components/maintenance_supporter/export.py index 8963415a..48b980a4 100644 --- a/custom_components/maintenance_supporter/export.py +++ b/custom_components/maintenance_supporter/export.py @@ -79,6 +79,9 @@ def _build_export_object( "created_at": tdata.get("created_at"), "archived_at": tdata.get("archived_at"), "archived_reason": tdata.get("archived_reason"), + # #193: a paused task stays paused after a restore. + "paused_at": tdata.get("paused_at"), + "paused_until": tdata.get("paused_until"), "schedule_type": sched["schedule_type"], "interval_days": sched["interval_days"], "interval_unit": sched["interval_unit"], @@ -163,6 +166,16 @@ def _build_export_object( # restored fleet task is a plain inspection task. if tdata.get(BATTERY_FLEET_TASK_FLAG): task[BATTERY_FLEET_TASK_FLAG] = True + # The fleet's replacement log and low latch live in the task's + # Store state, which merged_tasks does not overlay — a backup + # lost every recorded battery swap (round-trip audit 2026-09-29). + from .helpers.battery_fleet import LOW_LATCH_KEY + from .helpers.battery_lifetime import REPLACEMENT_LOG_KEY + + state = store.get_task_state(tid) if store is not None else {} + for key in (REPLACEMENT_LOG_KEY, LOW_LATCH_KEY): + if state.get(key): + task[key] = state[key] if include_history: task["history"] = tdata.get("history") or [] @@ -280,7 +293,7 @@ def serialize_export(data: dict[str, Any], fmt: str = "json") -> str: """ if fmt == "yaml": try: - import yaml # type: ignore[import-untyped] + import yaml # Normalize through JSON first: yaml.safe_dump rejects types the # JSON path coerces (e.g. tuples → lists), so YAML export would @@ -368,4 +381,11 @@ def build_settings_export(hass: HomeAssistant) -> dict[str, Any]: ): if key in opts: settings[key] = opts[key] - return {"version": 1, "global_settings": settings} + out: dict[str, Any] = {"version": 1, "global_settings": settings} + # Group members and vacation exemptions point at tasks by id; a move to + # another instance mints new ids, so name them (helpers.import_mapping). + from .helpers.import_mapping import TASK_NAMES_KEY, settings_task_names + + if task_names := settings_task_names(hass, settings): + out[TASK_NAMES_KEY] = task_names + return out diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts index 7a4d00b2..d36841ae 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts @@ -35,7 +35,6 @@ export interface ServiceCall { /** Same shape as `frontend-src/types.ts::AdvancedFeatures`. */ export interface MockFeatures { adaptive: boolean; - predictions: boolean; seasonal: boolean; environmental: boolean; budget: boolean; @@ -46,7 +45,7 @@ export interface MockFeatures { } export const DEFAULT_FEATURES: MockFeatures = { - adaptive: false, predictions: false, seasonal: false, + adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false, }; @@ -136,6 +135,8 @@ export interface CreateMockHassOptions { settingsResponse?: typeof DEFAULT_SETTINGS_RESPONSE; /** Per-WS-type handlers — return a value or Promise. Wins over built-in defaults. */ handlers?: Record; + /** Answers every WS type no handler and no built-in default covers. */ + fallback?: WsHandler; /** Optional `hass.services` registry (for ha-service-picker / schema-driven forms). */ services?: Record>; /** Optional `hass.states` (entity_id → state) — e.g. for notify-entity pickers. */ @@ -169,6 +170,7 @@ export function createMockHass(opts: CreateMockHassOptions = {}): CreateMockHass if (msg.type === "maintenance_supporter/users/list") return { users: [] }; if (msg.type === "maintenance_supporter/objects") return { objects: [] }; if (msg.type === "maintenance_supporter/tags/list") return { tags: [] }; + if (opts.fallback) return await opts.fallback(msg); return {}; }; diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/adopt-problem-sensors-dialog.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/adopt-problem-sensors-dialog.test.ts index 6685a974..aa416f40 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/adopt-problem-sensors-dialog.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/adopt-problem-sensors-dialog.test.ts @@ -54,7 +54,7 @@ describe("adopt-problem-sensors dialog: suggested part", () => { el.shadowRoot!.querySelectorAll("ha-button")[1].click(); // Adopt selected await el.updateComplete; await new Promise((r) => setTimeout(r, 0)); - const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as { + const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as unknown as { selections: Array<{ entity_id: string; part_id?: string }>; }; const byId = Object.fromEntries(adopt.selections.map((s) => [s.entity_id, s])); @@ -72,7 +72,7 @@ describe("adopt-problem-sensors dialog: flicker filter (#136)", () => { el.shadowRoot!.querySelectorAll("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 { + const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as unknown as { selections: Array<{ for_minutes?: number }>; }; expect(adopt.selections.every((s) => s.for_minutes === undefined)).to.equal(true); @@ -87,7 +87,7 @@ describe("adopt-problem-sensors dialog: flicker filter (#136)", () => { el.shadowRoot!.querySelectorAll("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 { + const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as unknown as { selections: Array<{ for_minutes?: number }>; }; expect(adopt.selections.length).to.be.greaterThan(0); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts index cdd96f6b..6957d30f 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts @@ -325,7 +325,7 @@ describe("complete-dialog", () => { }); }); -describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => { +describe("complete-dialog parts cost (#104)", () => { async function mountWithParts(over: Partial = {}) { const { hass } = createMockHass({}); const el = await fixture(html` @@ -345,54 +345,85 @@ describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => { const chip = (el: MaintenanceCompleteDialog) => el.shadowRoot!.querySelector(".cost-suggestion"); + const note = (el: MaintenanceCompleteDialog) => + el.shadowRoot!.querySelector(".cost-note"); - it("sums selected consumed parts (qty x unit cost) and fills on click", async () => { + const USING = { + parts: [ + { id: "p1", name: "Filter", cost: 12.5 }, + { id: "p2", name: "O-Ring", cost: 2.25 }, + { id: "p3", name: "Unpriced", cost: null }, + ] as never, + consumesParts: [ + { part_id: "p1", quantity: 1 }, + { part_id: "p2", quantity: 2 }, + { part_id: "p3", quantity: 1 }, + ] as never, + currencySymbol: "€", + }; + + it("parts used: their value is a line, never a suggestion into the cost field", async () => { + // 2.53 offered the parts' value as a one-click cost. A household that + // already booked the purchase counted the same filter twice. + const el = await mountWithParts(USING); + expect(chip(el), "no cost suggestion on a job that uses parts").to.equal(null); + expect(note(el)!.textContent).to.include("17 €"); + expect(note(el)!.textContent).to.include("when they were bought"); + }); + + it("parts used, counted when used: the line says they are booked", async () => { + const el = await mountWithParts({ ...USING, partsCostMode: "use" }); + expect(chip(el)).to.equal(null); + expect(note(el)!.textContent).to.include("17 €"); + expect(note(el)!.textContent).to.include("booked automatically"); + }); + + it("parts used: a package price is spread over its contents (#98)", async () => { const el = await mountWithParts({ - parts: [ - { id: "p1", name: "Filter", cost: 12.5 }, - { id: "p2", name: "O-Ring", cost: 2.25 }, - { id: "p3", name: "Unpriced", cost: null }, - ] as never, - consumesParts: [ - { part_id: "p1", quantity: 1 }, - { part_id: "p2", quantity: 2 }, - { part_id: "p3", quantity: 1 }, - ] as never, + parts: [{ id: "p1", name: "Spray", cost: 12, package_size: 400, unit: "ml" }] as never, + consumesParts: [{ part_id: "p1", quantity: 100 }] as never, currencySymbol: "€", }); - const c = chip(el)!; - expect(c, "suggestion chip rendered").to.exist; - expect(c.textContent).to.include("17 €"); - expect(c.textContent).to.include("€"); - c.click(); + // 100 ml of a 400 ml can at 12 € = 3 €, not 100 × 12 €. + expect(note(el)!.textContent).to.include("3 €"); + }); + + it("buy task: restock qty x price per package, follows the qty field, fills on click", async () => { + const el = await mountWithParts({ restockDefault: 2, restockUnitCost: 4.5 }); + expect(chip(el)!.textContent).to.include("9"); + setInput(el, 0, "4"); await el.updateComplete; - const cost = [...el.shadowRoot!.querySelectorAll(".field-input")][1]; - // Machine value for the — never profile-formatted - // ("17,00" would be rejected by the input under a decimal_comma profile). - expect(cost.value).to.equal("17"); + expect(chip(el)!.textContent).to.include("18"); + chip(el)!.click(); + await el.updateComplete; + // Buy task: restock qty, notes, cost. + const cost = [...el.shadowRoot!.querySelectorAll(".field-input")][2]; + // Machine value for the — never profile-formatted. + expect(cost.value).to.equal("18"); expect(chip(el), "chip hides once cost is set").to.equal(null); }); - it("buy task: restock qty x unit cost, follows the qty field", async () => { - const el = await mountWithParts({ restockDefault: 2, restockUnitCost: 4.5 }); - expect(chip(el)!.textContent).to.include("9"); + it("buy task, counted when used: the price becomes the part's price", async () => { + const el = await mountWithParts({ restockDefault: 2, restockUnitCost: 4.5, partsCostMode: "use" }); + expect(chip(el)).to.exist; + expect(note(el)!.textContent).to.include("price"); + const off = await mountWithParts({ restockDefault: 2, restockUnitCost: 4.5 }); + expect(note(off), "no hint when purchases are the spending").to.equal(null); }); - it("no chip when no involved part carries a price", async () => { + it("nothing when no involved part carries a price", async () => { const el = await mountWithParts({ parts: [{ id: "p1", name: "Filter", cost: null }] as never, consumesParts: [{ part_id: "p1", quantity: 1 }] as never, }); expect(chip(el)).to.equal(null); + expect(note(el)).to.equal(null); }); - it("no chip once the user typed a cost themselves", async () => { - const el = await mountWithParts({ - parts: [{ id: "p1", name: "Filter", cost: 5 }] as never, - consumesParts: [{ part_id: "p1", quantity: 1 }] as never, - }); + it("buy task: no chip once the user typed a cost themselves", async () => { + const el = await mountWithParts({ restockDefault: 1, restockUnitCost: 5 }); expect(chip(el)).to.exist; - setInput(el, 1, "3.10"); + setInput(el, 2, "3.10"); await el.updateComplete; expect(chip(el)).to.equal(null); }); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/dialog-no-lazy-load-elements.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/dialog-no-lazy-load-elements.test.ts index 5dbd2680..2c0e7fa1 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/dialog-no-lazy-load-elements.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/dialog-no-lazy-load-elements.test.ts @@ -78,7 +78,7 @@ describe("dialog tripwire: no lazy-loaded HA elements", () => { installation_date: "2025-01-01", documentation_url: "https://x.test/", notes: "test notes", }); - await (el as HTMLElement & { updateComplete: Promise }).updateComplete; + await (el as unknown as HTMLElement & { updateComplete: Promise }).updateComplete; const banned = findBannedTags(el.shadowRoot!); expect(banned, `object-dialog has banned lazy-load elements: ${banned.join(", ")}`) .to.have.lengthOf(0); @@ -106,10 +106,10 @@ describe("dialog tripwire: no lazy-loaded HA elements", () => { schedule_time: "09:00", on_complete_action: { service: "button.press", target: { entity_id: "button.x" } }, }); - await (el as HTMLElement & { updateComplete: Promise }).updateComplete; + await (el as unknown as HTMLElement & { updateComplete: Promise }).updateComplete; // Expand
sections so action UI is in the DOM el.shadowRoot!.querySelectorAll("details").forEach((d) => { d.open = true; }); - await (el as HTMLElement & { updateComplete: Promise }).updateComplete; + await (el as unknown as HTMLElement & { updateComplete: Promise }).updateComplete; const banned = findBannedTags(el.shadowRoot!); expect(banned, `task-dialog has banned lazy-load elements: ${banned.join(", ")}`) .to.have.lengthOf(0); @@ -123,7 +123,7 @@ describe("dialog tripwire: no lazy-loaded HA elements", () => { html``, ); el.openCreate(); - await (el as HTMLElement & { updateComplete: Promise }).updateComplete; + await (el as unknown as HTMLElement & { updateComplete: Promise }).updateComplete; const banned = findBannedTags(el.shadowRoot!); expect(banned, `group-dialog has banned lazy-load elements: ${banned.join(", ")}`) .to.have.lengthOf(0); @@ -138,7 +138,7 @@ describe("dialog tripwire: no lazy-loaded HA elements", () => { ); el.entryId = "entry_x"; el.taskId = "t1"; el.taskName = "Test"; el.open(); - await (el as HTMLElement & { updateComplete: Promise }).updateComplete; + await (el as unknown as HTMLElement & { updateComplete: Promise }).updateComplete; const banned = findBannedTags(el.shadowRoot!); expect(banned, `complete-dialog has banned lazy-load elements: ${banned.join(", ")}`) .to.have.lengthOf(0); @@ -156,7 +156,7 @@ describe("dialog tripwire: no lazy-loaded HA elements", () => { type: "completed", timestamp: "2025-01-01T00:00:00", notes: null, cost: null, duration: null, completed_by: null, }); - await (el as HTMLElement & { updateComplete: Promise }).updateComplete; + await (el as unknown as HTMLElement & { updateComplete: Promise }).updateComplete; const banned = findBannedTags(el.shadowRoot!); expect(banned, `history-edit-dialog has banned lazy-load elements: ${banned.join(", ")}`) .to.have.lengthOf(0); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/group-dialog-sort.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/group-dialog-sort.test.ts index 699000a2..7a34b183 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/group-dialog-sort.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/group-dialog-sort.test.ts @@ -99,7 +99,7 @@ describe("group-dialog task list (#40 alphabetical sort)", () => { await el.updateComplete; // Force a re-render with the same objects (ordering invariant). - el.objects = [...mockObjects()]; + el.objects = [...mockObjects()] as never; await el.updateComplete; const cbAgain = el.shadowRoot!.querySelectorAll(".object-block")[0] diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/ll-custom-payload.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/ll-custom-payload.test.ts index 75c315f2..8c3f336f 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/ll-custom-payload.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/ll-custom-payload.test.ts @@ -48,8 +48,10 @@ async function fireAndWait(detail: unknown): Promise { document.dispatchEvent( new CustomEvent("ll-custom", { detail, bubbles: true, composed: true }), ); - // handler imports dialog-mount.ts dynamically, then mounts the dialog - for (let i = 0; i < 40; i++) { + // handler imports dialog-mount.ts dynamically, then mounts the dialog — the + // first import compiles the whole dialog tree, which took over the old 1 s + // budget on a cold transform cache (failed three runs in a row, 2026-09-28) + for (let i = 0; i < 200; i++) { const dlg = document.body.querySelector(OBJECT_DIALOG_TAG); if (dlg) return dlg; await tick(25); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/panel-shell.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/panel-shell.test.ts index 71012ae1..4d97f8db 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/panel-shell.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/panel-shell.test.ts @@ -181,6 +181,44 @@ describe("panel shell", () => { expect(sr(el).querySelector(".today-person")).to.equal(null); }); + it("the person filter lists the responsible people and shows a saved view's person", async () => { + const { el } = await mountPanel( + [ + obj("e1", [ + task({ name: "Hers", responsible_user_id: "u-anna" }), + task({ name: "His", responsible_user_id: "u-ben" }), + task({ name: "Nobody's" }), + ]), + ], + { + "maintenance_supporter/users/list": () => ({ users: [{ id: "u-anna", name: "Anna" }, { id: "u-ben", name: "Ben" }, { id: "u-idle", name: "Idle" }] }), + "maintenance_supporter/views/list": () => ({ views: [{ id: "v-anna", name: "Anna's tasks", filters: { user_id: "u-anna" } }] }), + }, + ); + await new Promise((r) => setTimeout(r, 30)); + await el.updateComplete; + const userSelect = () => + [...sr(el).querySelectorAll("select")].find((s) => s.querySelector('option[value="current_user"]'))!; + // Everyone responsible for a task, by name — not people without tasks. + expect([...userSelect().options].map((o) => o.textContent!.trim()).slice(2)).to.deep.equal(["Anna", "Ben"]); + + // A saved view that narrows to one person shows that person — the select + // used to stay blank because only "All" and "My tasks" existed. + (el as unknown as { _applyView(id: string): void })._applyView("v-anna"); + await el.updateComplete; + expect(userSelect().selectedOptions[0].textContent!.trim()).to.equal("Anna"); + const rows = [...sr(el).querySelectorAll(".task-name")].map((n) => n.textContent!.trim()); + expect(rows.some((r) => r.includes("Hers"))).to.equal(true); + expect(rows.some((r) => r.includes("His") || r.includes("Nobody"))).to.equal(false); + + // Picking a person by hand filters the same way. + const sel = userSelect(); + sel.value = "u-ben"; + sel.dispatchEvent(new Event("change")); + await el.updateComplete; + expect((el as unknown as { _filterUser: string | null })._filterUser).to.equal("u-ben"); + }); + it("Today person chip stays readable on a phone next to a long object name (#169)", async () => { localStorage.setItem("msp-overview-tab", "today"); await setViewport({ width: 360, height: 800 }); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/suggested-setups-baseline.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/suggested-setups-baseline.test.ts index 92de8026..6d85562c 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/suggested-setups-baseline.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/suggested-setups-baseline.test.ts @@ -83,7 +83,7 @@ describe("suggested-setups dialog: counting start value (#102)", () => { await el.updateComplete; await new Promise((r) => setTimeout(r, 0)); - const adopt = sent.find((m) => m.type === "maintenance_supporter/integration_setups/adopt")! as { + const adopt = sent.find((m) => m.type === "maintenance_supporter/integration_setups/adopt")! as unknown as { selections: Array<{ device_id: string; entry_id?: string }>; }; const byId = Object.fromEntries(adopt.selections.map((s) => [s.device_id, s])); @@ -102,7 +102,7 @@ describe("suggested-setups dialog: counting start value (#102)", () => { await el.updateComplete; await new Promise((r) => setTimeout(r, 0)); - const adopt = sent.find((m) => m.type === "maintenance_supporter/integration_setups/adopt")! as { + const adopt = sent.find((m) => m.type === "maintenance_supporter/integration_setups/adopt")! as unknown as { selections: Array<{ device_id: string; baselines?: Record }>; }; const byId = Object.fromEntries(adopt.selections.map((s) => [s.device_id, s])); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts index 7694df4f..f1ac1b09 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts @@ -48,7 +48,7 @@ function task(overrides: Record = {}): MaintenanceTask { function ctx(overrides: Partial = {}): TaskDetailContext { const { hass } = createMockHass({ - handler: () => ({ documents: [] }), + fallback: () => ({ documents: [] }), }); return { lang: "en", @@ -59,13 +59,15 @@ function ctx(overrides: Partial = {}): TaskDetailContext { objectDocUrl: null, objectManualDocs: [], openManualDoc: () => {}, + setPhaseCursor: () => {}, + moveTask: () => {}, setChecklistItem: () => {}, isOperator: false, actionLoading: false, moreMenuOpen: false, activeTab: "overview", features: { - adaptive: false, predictions: false, seasonal: false, + adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false, }, @@ -105,6 +107,7 @@ function ctx(overrides: Partial = {}): TaskDetailContext { openComplete: () => undefined, promptSkip: () => undefined, toggleArchive: () => undefined, + togglePause: () => undefined, openQr: () => undefined, duplicateTask: () => undefined, promptReset: () => undefined, @@ -178,7 +181,7 @@ describe("task-detail renderer", () => { expect(items.join(" ")).to.not.match(/edit|archive|delete|duplicate|move/i); }); - it("open more-menu lists edit/qr/worksheet/duplicate/reset/postpone/snooze/archive/delete and fires callbacks", () => { + it("open more-menu lists edit/qr/worksheet/duplicate/reset/postpone/snooze/pause/archive/delete and fires callbacks", () => { const calls: string[] = []; const host = mount(task(), ctx({ moreMenuOpen: true, @@ -187,21 +190,36 @@ describe("task-detail renderer", () => { promptPostpone: () => calls.push("postpone"), snoozeTask: () => calls.push("snooze"), printWorksheet: () => calls.push("worksheet"), + togglePause: (paused) => calls.push(paused ? "resume" : "pause"), toggleArchive: () => calls.push("archive"), })); const items = [...host.querySelectorAll(".popup-menu-item")]; - // edit, qr, worksheet, duplicate, move (#23), reset, postpone, snooze, archive, delete - expect(items.length).to.equal(10); + // edit, qr, worksheet, duplicate, move (#23), reset, postpone, snooze, pause (#193), archive, delete + expect(items.length).to.equal(11); (items[6] as HTMLElement).click(); // postpone (items[7] as HTMLElement).click(); // snooze (items[2] as HTMLElement).click(); // work sheet (v2.21) - (items[8] as HTMLElement).click(); // archive (demoted from the header) - (items[9] as HTMLElement).click(); // delete (danger) + (items[8] as HTMLElement).click(); // pause this one task (#193) + (items[9] as HTMLElement).click(); // archive (demoted from the header) + (items[10] as HTMLElement).click(); // delete (danger) expect(calls).to.deep.equal([ - "close", "postpone", "close", "snooze", "close", "worksheet", "close", "archive", "close", "delete", + "close", "postpone", "close", "snooze", "close", "worksheet", "close", "pause", "close", "archive", "close", "delete", ]); }); + it("a paused task offers Resume and names its resume date (#193)", () => { + const calls: string[] = []; + const host = mount(task({ paused: true, paused_until: "2027-03-01", status: "paused" } as never), ctx({ + moreMenuOpen: true, + togglePause: (paused) => calls.push(paused ? "resume" : "pause"), + })); + const resume = [...host.querySelectorAll(".popup-menu-item")].find((i) => i.textContent?.trim() === "Resume"); + expect(resume, "Resume offered").to.exist; + resume!.click(); + expect(calls).to.deep.equal(["resume"]); + expect(host.querySelector(".paused-until")?.textContent).to.include("until"); + }); + it("shows a bell-off badge for a muted task (#173), and none otherwise", () => { const muted = mount(task({ notify_enabled: false }), ctx()); expect(muted.querySelector(".muted-badge")).to.exist; @@ -277,7 +295,7 @@ describe("task-detail renderer", () => { task({ checklist: ["Drain", "Clean", "Refill"], checklist_progress: { Clean: true } }), ctx({ features: { - adaptive: false, predictions: false, seasonal: false, + adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: true, schedule_time: false, completion_actions: false, }, diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts index 4426d3d1..848ebca3 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts @@ -45,7 +45,7 @@ function task(overrides: Record = {}): MaintenanceTask { function ctx(overrides: Partial = {}): TaskDetailContext { const { hass } = createMockHass({ - handler: () => ({ documents: [] }), + fallback: () => ({ documents: [] }), }); return { lang: "en", @@ -56,13 +56,15 @@ function ctx(overrides: Partial = {}): TaskDetailContext { objectDocUrl: null, objectManualDocs: [], openManualDoc: () => {}, + setPhaseCursor: () => {}, + moveTask: () => {}, setChecklistItem: () => {}, isOperator: false, actionLoading: false, moreMenuOpen: false, activeTab: "overview", features: { - adaptive: false, predictions: false, seasonal: false, + adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false, }, @@ -102,6 +104,7 @@ function ctx(overrides: Partial = {}): TaskDetailContext { openComplete: () => undefined, promptSkip: () => undefined, toggleArchive: () => undefined, + togglePause: () => undefined, openQr: () => undefined, duplicateTask: () => undefined, promptReset: () => undefined, diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-calendar-kinds.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-calendar-kinds.test.ts index 05778aba..1fa27800 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-calendar-kinds.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-calendar-kinds.test.ts @@ -105,7 +105,7 @@ describe("task-dialog calendar kinds (Phase 4)", () => { (el as any)._calOffset = "-2"; await (el as any)._save(); const msg = sent.find((m: any) => m.type === "maintenance_supporter/task/create"); - expect(msg.schedule).to.deep.equal({ + expect(msg!.schedule).to.deep.equal({ kind: "day_of_month", day: -1, business: true, offset: -2, }); }); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-runtime-onstates.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-runtime-onstates.test.ts index 97a2a39b..cce96f4f 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-runtime-onstates.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-runtime-onstates.test.ts @@ -55,7 +55,7 @@ describe("task-dialog runtime on-states (#103)", () => { await el.openEdit("entry_x", MOWER_TASK as any); await el.updateComplete; await (el as any)._save(); - const update = sent.find((m) => m.type === "maintenance_supporter/task/update")! as { + const update = sent.find((m) => m.type === "maintenance_supporter/task/update")! as unknown as { trigger_config: { trigger_on_states?: string[] }; }; expect(update.trigger_config.trigger_on_states).to.deep.equal(["mowing"]); @@ -68,7 +68,7 @@ describe("task-dialog runtime on-states (#103)", () => { (el as any)._triggerOnStates = ""; await el.updateComplete; await (el as any)._save(); - const update = sent.find((m) => m.type === "maintenance_supporter/task/update")! as { + const update = sent.find((m) => m.type === "maintenance_supporter/task/update")! as unknown as { trigger_config: { trigger_on_states?: string[] }; }; expect(update.trigger_config.trigger_on_states).to.equal(undefined); diff --git a/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts b/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts index 9b55bc90..cd31b773 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts @@ -9,6 +9,8 @@ import { property, state } from "lit/decorators.js"; import { t, ensureLocale, langOf, formatDate, syncLocaleFromHass } from "../styles"; import { LS_KEYS, lsGet, lsSet } from "../helpers/storage-keys"; import { runWs } from "../helpers/ws-run"; +import { canWrite, NO_DELEGATION, type WriteAccess } from "../helpers/permissions"; +import { fetchSettingsOnce } from "../helpers/settings-cache"; import { isoDateLocal } from "../helpers/calendar-bucket"; import { px } from "../renderers/chart-utils"; import type { HomeAssistant } from "../types"; @@ -107,6 +109,12 @@ export class MaintenanceBatteryFleetSection extends LitElement { @state() private _rosterSort: "name" | "urgency" = MaintenanceBatteryFleetSection._storedSort(); @state() private _typeFilter: string | null = null; @state() private _recorded: string[] = []; + /** Every fleet action (mark replaced, record, exclude/include, add, the two + * options, repair) is WRITE tier on the server; the section offered them + * to every household member, who then got "not permitted" and a checkbox + * left flipped (audit 2026-09-29). Same rule as the panel and the other + * cards: helpers/permissions.canWrite. */ + @state() private _access: WriteAccess = NO_DELEGATION; private _historyRequested = false; private _localeReady = false; @@ -114,6 +122,10 @@ export class MaintenanceBatteryFleetSection extends LitElement { return langOf(this.hass); } + private get _canWrite(): boolean { + return canWrite(this.hass?.user, this._access); + } + connectedCallback(): void { super.connectedCallback(); if (this.hass) this._load(); @@ -128,6 +140,7 @@ export class MaintenanceBatteryFleetSection extends LitElement { if (changed.has("hass") && this.hass && !this._localeReady) { this._localeReady = true; ensureLocale(this._lang).then(() => this.requestUpdate()); + void fetchSettingsOnce(this.hass).then((s) => { this._access = s.access; }); if (this._ov === null && !this._loading) this._load(); } } @@ -432,7 +445,8 @@ export class MaintenanceBatteryFleetSection extends LitElement { ? html`${t("battery_fleet_no_sensor", L)}` : nothing; const type = html`${b.quantity}× ${b.battery_type}`; - const showMark = o.mark === "always" || b.no_sensor || b.can_mark_replaced; + const writer = this._canWrite; + const showMark = writer && (o.mark === "always" || b.no_sensor || b.can_mark_replaced); return html`
${b.device_name} @@ -461,7 +475,7 @@ export class MaintenanceBatteryFleetSection extends LitElement { ` : nothing} - ${o.jump ? this._jumpButton(b, L) : nothing} + ${o.jump && writer ? this._jumpButton(b, L) : nothing} ${o.predicted && b.days_until != null ? html`${b.forecast_overdue ? html`` : nothing}~${this._predictedDate(b.days_until)}` : nothing} - ${o.exclude + ${o.exclude && writer ? html`
` : nothing} @@ -521,11 +537,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
${ov.low.map((b) => this._renderRow(b, L, { recharge: true, mark: "always", exclude: true }))}
-
- - ${t("battery_fleet_mark_all", L)} - -
+ ${this._canWrite + ? html`
+ + ${t("battery_fleet_mark_all", L)} + +
` + : nothing} `} ${ov.soon.length @@ -564,6 +582,19 @@ export class MaintenanceBatteryFleetSection extends LitElement { )}
${t("battery_fleet_all_hint", L)}
+ ${this._canWrite ? this._renderRosterSettings(ov, L) : nothing} +
+ ` + : nothing} + ${this._renderExcluded(ov, L)} +
${t("battery_fleet_total", L).replace("{n}", String(ov.total))}
+ + `; + } + + /** Add a battery + the two fleet options — write tier, so writers only. */ + private _renderRosterSettings(ov: Overview, L: string) { + return html`
${t("battery_fleet_add", L)}
${t("battery_fleet_due_without_sensor_hint", L)}
- - ` - : nothing} + `; + } + + /** Excluded devices stay listed for everyone; bringing one back is write tier. */ + private _renderExcluded(ov: Overview, L: string) { + return html` ${ov.excluded?.length ? html`
@@ -605,22 +639,22 @@ export class MaintenanceBatteryFleetSection extends LitElement { (x) => html` ${x.device_name} - + ${this._canWrite + ? html`` + : nothing} `, )}
` : nothing} -
${t("battery_fleet_total", L).replace("{n}", String(ov.total))}
-
`; } diff --git a/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts index 68fdf6f9..25afc5fc 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts @@ -9,6 +9,7 @@ import { runWs } from "../helpers/ws-run"; import { isoMinuteLocal } from "../helpers/calendar-bucket"; import { PART_QTY_RANGE, RESTOCK_QTY_RANGE } from "../helpers/setting-ranges"; import { partLinkKey, type LinkedPart } from "../helpers/shared-parts"; +import { unitPrice, type PartsCostMode } from "../helpers/parts-cost"; import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels"; import { PhotoUploadController } from "../helpers/photo-upload-controller"; import { parseDurationMinutes } from "../helpers/duration"; @@ -45,8 +46,12 @@ export class MaintenanceCompleteDialog extends LitElement { /** #104 follow-up: the buy task's part unit cost — powers the cost * suggestion (restock qty × unit cost). */ @property({ attribute: false }) public restockUnitCost: number | null = null; + /** #98: one package of the part ("400 ml") — the quantity counts packages. */ + @property({ attribute: false }) public restockPackage = ""; /** Currency symbol for the cost suggestion ("" = plain number). */ @property() public currencySymbol = ""; + /** #104: when spare parts count ("purchase" | "use"). */ + @property({ attribute: false }) public partsCostMode: PartsCostMode = "purchase"; /** #99: the parts offered on completion — enables the editable "parts used" * section. Built by `partsForCompletion`: the object's own inventory plus * every shared pool this task links to (#111), each tagged with its owner. */ @@ -309,45 +314,60 @@ export class MaintenanceCompleteDialog extends LitElement { return this.requiredFields.includes(field) ? html`` : nothing; } - /** #104 follow-up: suggested cost derived from the parts this completion - * touches — the SELECTED "parts used" (qty × each part's unit cost) on a - * consuming task, or restock qty × unit cost on a buy task. Null when no - * involved part carries a price. Follows the live selection, so ticking - * a part off updates the suggestion. */ - private _partsCostSuggestion(): number | null { - if (this.restockDefault !== null) { - const qty = parseFloat(this._restockQty); - if (this.restockUnitCost == null || !Number.isFinite(qty) || qty <= 0) return null; - return Math.round(this.restockUnitCost * qty * 100) / 100; - } - if (!this.parts.length) return null; + /** A buy task's price: restock qty (packages) × the part's price per + * package. Null without a price. */ + private _purchaseSuggestion(): number | null { + if (this.restockDefault === null) return null; + const qty = parseFloat(this._restockQty); + if (this.restockUnitCost == null || !Number.isFinite(qty) || qty <= 0) return null; + return Math.round(this.restockUnitCost * qty * 100) / 100; + } + + /** #104: the value of the SELECTED parts used (qty × each part's price per + * unit — a package price spread over its contents, #98). Follows the live + * selection. Null when no selected part carries a price. */ + private _partsValue(): number | null { + if (this.restockDefault !== null || !this.parts.length) return null; let sum = 0; let priced = false; for (const link of Object.values(this._usedParts)) { const def = this.parts.find( (pt) => partLinkKey({ part_id: pt.id, entry_id: pt.entry_id }) === partLinkKey(link), ); - if (def?.cost != null) { - sum += def.cost * (link.quantity || 1); + const price = unitPrice(def); + if (price !== null) { + sum += price * (link.quantity || 1); priced = true; } } return priced ? Math.round(sum * 100) / 100 : null; } - /** The one-click "use ≈ X from parts" chip under the cost field. Hidden - * once the user typed a cost themselves — a suggestion, never an - * overwrite. */ + /** Under the cost field. A buy task gets the one-click price suggestion + * (and, when parts count when used, the note that the price becomes the + * part's price). A completion that USES parts gets their value as a line + * — booked automatically when parts count when used, or named as counted + * at purchase. Suggesting it into the cost field (2.53) double-counted a + * household that books its purchases (Discussion #104). */ private _renderCostSuggestion(L: string) { - if (this._cost.trim() !== "") return nothing; - const suggestion = this._partsCostSuggestion(); - if (suggestion == null || suggestion <= 0) return nothing; - const amount = formatCost(suggestion, this.currencySymbol, L); - return html``; + const purchase = this._purchaseSuggestion(); + if (this.restockDefault !== null) { + const hint = this.partsCostMode === "use" + ? html`
${t("cost_purchase_use_hint", L)}
` + : nothing; + if (this._cost.trim() !== "" || purchase == null || purchase <= 0) return hint; + const amount = formatCost(purchase, this.currencySymbol, L); + return html`${hint}`; + } + const value = this._partsValue(); + if (value == null || value <= 0) return nothing; + const amount = formatCost(value, this.currencySymbol, L); + const key = this.partsCostMode === "use" ? "cost_parts_booked" : "cost_parts_info"; + return html`
${t(key, L).replace("{amount}", amount)}
`; } private _close(): void { @@ -452,7 +472,7 @@ export class MaintenanceCompleteDialog extends LitElement { ${this.restockDefault !== null ? html`
this._editing=null}>${a("cancel",t)} @@ -2782,21 +2807,21 @@ ${k(l.notes)}
`:""} > - `}_inventoryValue(){let t=0,e=!1;for(let i of this.parts){let s=typeof i.cost=="number"?i.cost:null,n=typeof i.stock=="number"?i.stock:null;s!==null&&n!==null&&(t+=s*n,e=!0)}return e?t:null}render(){let t=this._lang;return!this.parts.length&&!this.canWrite?p:o` + `}_inventoryValue(){let t=0,e=!1;for(let i of this.parts){let s=Ze(i),o=typeof i.stock=="number"?i.stock:null;s!==null&&o!==null&&(t+=s*o,e=!0)}return e?t:null}render(){let t=this._lang;return!this.parts.length&&!this.canWrite?h:r`

${a("parts_section",t)} (${this.parts.length}) - ${this._inventoryValue()!==null?o`${a("parts_inventory_value",t)}: - ${Q(this._inventoryValue(),this.currencySymbol,t)}`:p} + ${U(this._inventoryValue(),this.currencySymbol,t)}`:h}

- ${this.canWrite&&!this._editing?o`this._openAdd()}> + ${this.canWrite&&!this._editing?r`this._openAdd()}> ${a("part_add",t)} - `:p} + `:h}
- ${this._error?o`
${this._error}
`:p} - ${this._editing?this._renderForm():p} + ${this._error?r`
${this._error}
`:h} + ${this._editing?this._renderForm():h} ${this.parts.map(e=>this._renderRow(e))} `}};ot.styles=N` :host { @@ -2897,6 +2922,12 @@ ${k(l.notes)}`:""} grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px 12px; } + .form-hint { + grid-column: 1 / -1; + font-size: 12px; + color: var(--secondary-text-color); + line-height: 1.4; + } .form-field { display: flex; flex-direction: column; @@ -2912,6 +2943,25 @@ ${k(l.notes)}`:""} background: var(--card-background-color); color: var(--primary-text-color); } + .form-field.notes { + grid-column: 1 / -1; + } + .form-field textarea { + padding: 6px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + font: inherit; + resize: vertical; + } + .part-notes { + font-size: 12px; + color: var(--secondary-text-color); + white-space: pre-line; + overflow-wrap: anywhere; + margin-top: 2px; + } .form-field.checkbox { flex-direction: row; align-items: center; @@ -2929,31 +2979,31 @@ ${k(l.notes)}`:""} font-size: 13px; margin: 4px 0; } - `,g([R({attribute:!1})],ot.prototype,"hass",2),g([R({attribute:!1})],ot.prototype,"entryId",2),g([R({attribute:!1})],ot.prototype,"parts",2),g([R({type:Boolean})],ot.prototype,"canWrite",2),g([R({attribute:!1})],ot.prototype,"currencySymbol",2),g([_()],ot.prototype,"_editing",2),g([_()],ot.prototype,"_busy",2),g([_()],ot.prototype,"_error",2),g([_()],ot.prototype,"_restockFor",2),g([_()],ot.prototype,"_restockQty",2),g([_()],ot.prototype,"_restockInvalid",2),g([_()],ot.prototype,"_docsFor",2);customElements.get("maintenance-parts-section")||customElements.define("maintenance-parts-section",ot);var _e={readings:!0,parts:!0,photos:!0,documents:!0,checklist:!0,notes:!0,costs:!0,person:!0,refs:!0,qr:!1,bare:!0,docDescriptions:!0};function _s(l){return!!(l.notes&&l.notes.trim()||l.cost!=null||l.duration!=null||l.readings.length||l.parts.length||l.photoIds.length||l.checklist)}var vs=6;function Bi(l,d,t,e,i,s,n,c={}){let r=c.options??{layout:"chronological",include:_e},u=r.include,h=c.data??{objectRef:null,tasks:[],photos:{},fmtNumber:$=>String($)},m=d.filter($=>$.type==="completed"&&(u.bare!==!1||_s($))),{totalCost:v}=oe(m),b=$=>u.refs&&h.objectRef&&$.taskRefNo!=null&&$.refNo!=null?`${h.objectRef}.${$.taskRefNo}-${$.refNo}`:null,y=$=>u.refs&&h.objectRef&&$.taskRefNo!=null?`${h.objectRef}.${$.taskRefNo}`:null,M=[[t.refNumber,u.refs&&h.objectRef?`#${h.objectRef}`:null],[t.manufacturer,l.manufacturer],[t.model,l.model],[t.serial,l.serial_number],[t.installed,l.installation_date?e(l.installation_date):null]].filter(([,$])=>$).map(([$,L])=>`
${k($)}${k(L)}
`).join(""),j=$=>{let L=[];if(u.readings&&$.readings.length){let H=$.readings.map(I=>{let W=`${h.fmtNumber(I.value)}${I.unit?` ${k(I.unit)}`:""}`,X=I.delta!=null?` (${I.delta>=0?"+":"\u2212"}${h.fmtNumber(Math.abs(I.delta))})`:"";return`${I.name?`${k(I.name)}: `:""}${W}${X}`});L.push(`
${k(t.readings)}${H.join(" \xB7 ")}
`)}if(u.parts&&$.parts.length&&L.push(`
${k(t.parts)}${$.parts.map(H=>`${k(H.name)} \xD7 ${h.fmtNumber(H.quantity)}`).join(", ")}
`),u.checklist&&$.checklist&&L.push(`
${k(t.checklist)}${$.checklist.done}/${$.checklist.total}
`),u.photos&&$.photoIds.length){let H=$.photoIds.slice(0,vs),I=H.map(X=>{let U=h.photos[X],S=U?.name||X.slice(0,8);return U?.url?`
${k(S)}
`:`
${k(S)}
`}),W=$.photoIds.length>H.length?`+${$.photoIds.length-H.length}`:"";L.push(`
${k(t.photos)}
${I.join("")}${W}
`)}return L.length?`
${L.join("")}
`:""},E=$=>[u.notes?$.notes:null,u.person&&$.completedBy?`${t.completedBy}: ${$.completedBy}`:null].filter(Boolean).join(" \xB7 "),x=$=>u.costs?`${$.cost!=null?k(s($.cost)):k(t.none)} - ${$.duration!=null?k(i($.duration)):k(t.none)}`:"",D=$=>$?`#${k($)}`:"",C=u.costs?5:3,f=$=>` + `,g([R({attribute:!1})],ot.prototype,"hass",2),g([R({attribute:!1})],ot.prototype,"entryId",2),g([R({attribute:!1})],ot.prototype,"parts",2),g([R({type:Boolean})],ot.prototype,"canWrite",2),g([R({attribute:!1})],ot.prototype,"currencySymbol",2),g([m()],ot.prototype,"_editing",2),g([m()],ot.prototype,"_busy",2),g([m()],ot.prototype,"_error",2),g([m()],ot.prototype,"_restockFor",2),g([m()],ot.prototype,"_restockQty",2),g([m()],ot.prototype,"_restockInvalid",2),g([m()],ot.prototype,"_docsFor",2);customElements.get("maintenance-parts-section")||customElements.define("maintenance-parts-section",ot);var fe={readings:!0,parts:!0,photos:!0,documents:!0,checklist:!0,notes:!0,costs:!0,person:!0,refs:!0,qr:!1,bare:!0,docDescriptions:!0};function ks(l){return!!(l.notes&&l.notes.trim()||l.cost!=null||l.duration!=null||l.readings.length||l.parts.length||l.photoIds.length||l.checklist)}var $s=6;function Wi(l,d,t,e,i,s,o,c={}){let n=c.options??{layout:"chronological",include:fe},u=n.include,p=c.data??{objectRef:null,tasks:[],photos:{},fmtNumber:$=>String($)},_=d.filter($=>$.type==="completed"&&(u.bare!==!1||ks($))),{totalCost:v}=le(_),f=$=>u.refs&&p.objectRef&&$.taskRefNo!=null&&$.refNo!=null?`${p.objectRef}.${$.taskRefNo}-${$.refNo}`:null,y=$=>u.refs&&p.objectRef&&$.taskRefNo!=null?`${p.objectRef}.${$.taskRefNo}`:null,M=[[t.refNumber,u.refs&&p.objectRef?`#${p.objectRef}`:null],[t.manufacturer,l.manufacturer],[t.model,l.model],[t.serial,l.serial_number],[t.installed,l.installation_date?e(l.installation_date):null]].filter(([,$])=>$).map(([$,L])=>`
${k($)}${k(L)}
`).join(""),j=$=>{let L=[];if(u.readings&&$.readings.length){let H=$.readings.map(z=>{let G=`${p.fmtNumber(z.value)}${z.unit?` ${k(z.unit)}`:""}`,X=z.delta!=null?` (${z.delta>=0?"+":"\u2212"}${p.fmtNumber(Math.abs(z.delta))})`:"";return`${z.name?`${k(z.name)}: `:""}${G}${X}`});L.push(`
${k(t.readings)}${H.join(" \xB7 ")}
`)}if(u.parts&&$.parts.length&&L.push(`
${k(t.parts)}${$.parts.map(H=>`${k(H.name)} \xD7 ${p.fmtNumber(H.quantity)}`).join(", ")}
`),u.checklist&&$.checklist&&L.push(`
${k(t.checklist)}${$.checklist.done}/${$.checklist.total}
`),u.photos&&$.photoIds.length){let H=$.photoIds.slice(0,$s),z=H.map(X=>{let V=p.photos[X],S=V?.name||X.slice(0,8);return V?.url?`
${k(S)}
`:`
${k(S)}
`}),G=$.photoIds.length>H.length?`+${$.photoIds.length-H.length}`:"";L.push(`
${k(t.photos)}
${z.join("")}${G}
`)}return L.length?`
${L.join("")}
`:""},E=$=>[u.notes?$.notes:null,u.person&&$.completedBy?`${t.completedBy}: ${$.completedBy}`:null].filter(Boolean).join(" \xB7 "),x=$=>u.costs?`${$.cost!=null?k(s($.cost)):k(t.none)} + ${$.duration!=null?k(i($.duration)):k(t.none)}`:"",D=$=>$?`#${k($)}`:"",C=u.costs?5:3,b=$=>` ${k(t.colDate)} ${$?`${k(t.colTask)}`:""} ${u.costs?`${k(t.colCost)}${k(t.colDuration)}`:""} ${k(t.colNotes)} - `,it=($,L)=>{let H=E($),I=j($),W=L?k($.phaseName?`${$.taskName} \xB7 ${$.phaseName}`:$.taskName):k($.phaseName||"");return` - ${k(e($.timestamp))}${D(b($))} - ${L?`${W} ${D(y($))}`:W} + `,it=($,L)=>{let H=E($),z=j($),G=L?k($.phaseName?`${$.taskName} \xB7 ${$.phaseName}`:$.taskName):k($.phaseName||"");return` + ${k(e($.timestamp))}${D(f($))} + ${L?`${G} ${D(y($))}`:G} ${x($)} - ${k(H)||(I?"":k(t.none))} - ${I?`${I}`:""}`},ht;if(r.layout==="by_task"){let $=new Map(h.tasks.map(I=>[I.id,I])),L=[...h.tasks.map(I=>I.id)];for(let I of m)L.includes(I.taskId)||L.push(I.taskId);ht=`${L.map(I=>{let W=m.filter(ut=>ut.taskId===I);if(!W.length)return"";let X=$.get(I),U=X?.name||W[0].taskName,S=u.refs?X?.ref??y(W[0]):null,T=u.documents&&X?.documents.length?`
${k(t.documents)}${X.documents.map(ut=>`${k(ut.title)}${ut.page?` (${k(t.page(ut.page))})`:""}${u.docDescriptions!==!1&&ut.description?` \u2014 ${k(ut.description)}`:""}`).join(", ")}
`:"",z=u.qr&&X?.qrDataUri?`
${k(t.scanHint)}
`:"",Ot=W.reduce((ut,os)=>ut+(os.cost??0),0);return`
+ ${k(H)||(z?"":k(t.none))} + ${z?`${z}`:""}`},ht;if(n.layout==="by_task"){let $=new Map(p.tasks.map(z=>[z.id,z])),L=[...p.tasks.map(z=>z.id)];for(let z of _)L.includes(z.taskId)||L.push(z.taskId);ht=`${L.map(z=>{let G=_.filter(ut=>ut.taskId===z);if(!G.length)return"";let X=$.get(z),V=X?.name||G[0].taskName,S=u.refs?X?.ref??y(G[0]):null,T=u.documents&&X?.documents.length?`
${k(t.documents)}${X.documents.map(ut=>`${k(ut.title)}${ut.page?` (${k(t.page(ut.page))})`:""}${u.docDescriptions!==!1&&ut.description?` \u2014 ${k(ut.description)}`:""}`).join(", ")}
`:"",I=u.qr&&X?.qrDataUri?`
${k(t.scanHint)}
`:"",Ot=G.reduce((ut,us)=>ut+(us.cost??0),0);return`
-

${k(U)} ${D(S)}

+

${k(V)} ${D(S)}

${X?.schedule?`
${k(X.schedule)}
`:""} ${T} -
${k(t.entriesLabel(W.length))}${u.costs&&Ot>0?` \xB7 ${k(s(Ot))}`:""}
+
${k(t.entriesLabel(G.length))}${u.costs&&Ot>0?` \xB7 ${k(s(Ot))}`:""}
- ${z} + ${I}
- ${f(!1)} + ${b(!1)} -${W.map(ut=>it(ut,!1)).join(` +${G.map(ut=>it(ut,!1)).join(` `)}
@@ -2965,9 +3015,9 @@ ${W.map(ut=>it(ut,!1)).join(` ${u.costs?k(s(v)):""} `}else ht=` - ${f(!0)} + ${b(!0)} -${m.map($=>it($,!0)).join(` +${_.map($=>it($,!0)).join(` `)} @@ -3028,24 +3078,24 @@ ${m.map($=>it($,!0)).join(`

${k(t.title)} \u2014 ${k(l.name)}

-

${k(t.generated)} ${k(e(n))} \xB7 ${k(t.entriesLabel(m.length))}

+

${k(t.generated)} ${k(e(o))} \xB7 ${k(t.entriesLabel(_.length))}

${M?`
${M}
`:""} ${ht} ${c.capped?`

${k(t.capNote)}

`:""} -`}var bs=60,tt=class extends q{constructor(){super(...arguments);this.entryId="";this.object=null;this.tasks=[];this.currencySymbol="\u20AC";this.userName=()=>null;this._full={};this._loading=!1;this._filterTask="";this._from="";this._to="";this._expanded=!1;this._printOpen=!1;this._printLayout="chronological";this._printInclude={..._e};this._printing=!1;this._loadedFor=null;this._loadedSignature="";this._loadSeq=0;this._localeReady=!1}connectedCallback(){super.connectedCallback();try{let t=JSON.parse(Z(A.printOptions)||"null");(t?.layout==="by_task"||t?.layout==="chronological")&&(this._printLayout=t.layout),t?.include&&typeof t.include=="object"&&(this._printInclude={..._e,...t.include})}catch{}}_savePrintOptions(){V(A.printOptions,JSON.stringify({layout:this._printLayout,include:this._printInclude}))}_toggleInclude(t,e){this._printInclude={...this._printInclude,[t]:e},this._savePrintOptions()}get _lang(){return K(this.hass)}updated(t){if(super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,at(this._lang).then(()=>this.requestUpdate())),this.entryId&&this._loadedFor!==this.entryId)this._loadedFor=this.entryId,this._full={},this._filterTask="",this._from="",this._to="",this._loadedSignature=this._historySignature(),this._loadFullHistories();else if(this.entryId&&t.has("tasks")){let e=this._historySignature();e!==this._loadedSignature&&(this._loadedSignature=e,this._loadFullHistories())}}_historySignature(){return JSON.stringify(this.tasks.map(t=>[t.id,t.history_count??null,t.history??[]]))}async _loadFullHistories(){let t=this.entryId,e=this.tasks,i=++this._loadSeq;if(!e.length){this._full={},this._loading=!1;return}this._loading=!0;let s=await Promise.all(e.map(async n=>{try{let c=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/history",entry_id:t,task_id:n.id});return[n.id,c.history??[]]}catch{return[n.id,n.history??[]]}}));this.entryId!==t||i!==this._loadSeq||(this._full=Object.fromEntries(s),this._loading=!1)}get _entries(){return li(this.tasks.map(t=>({id:t.id,name:t.name,history:this._full[t.id]??t.history??[],ref_no:t.ref_no,reading_unit:t.reading_unit})))}get _capped(){return Object.values(this._full).some(t=>t.length>=ni)}_openTask(t){this.dispatchEvent(new CustomEvent("open-task",{detail:{taskId:t},bubbles:!0,composed:!0}))}async _bookletData(t){let e=this._printInclude,i=this._lang,s=[];if(e.documents||e.photos)try{s=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/documents/list",entry_id:this.entryId})).documents||[]}catch{s=[]}let n=new Map;e.qr&&this._printLayout==="by_task"&&await Promise.all(this.tasks.map(async u=>{try{let h=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/qr/generate",entry_id:this.entryId,task_id:u.id,url_mode:"server",action:"view"});h.svg_data_uri&&n.set(u.id,h.svg_data_uri)}catch{}}));let c=this.tasks.map(u=>({id:u.id,name:u.name,ref:yt(this.object,u),schedule:St(u,i)||null,documents:s.filter(h=>(h.task_ids||[]).includes(u.id)&&!(h.tags||[]).includes("photo")).map(h=>({title:rt(h),page:h.task_pages?.[u.id]??null,description:h.description||null})),qrDataUri:n.get(u.id)??null})),r={};if(e.photos){let u=[...new Set(t.filter(m=>m.type==="completed").flatMap(m=>m.photoIds))].slice(0,bs),h=new Map(s.map(m=>[m.id,m]));await Promise.all(u.map(async m=>{let v=h.get(m),b=v&&rt(v)||m.slice(0,8);try{r[m]={name:b,url:new URL(await te(this.hass,m),window.location.origin).href}}catch{r[m]={name:b,url:null}}}))}return{tasks:c,photos:r}}async _print(t){let e=this._lang,i=this.object;if(!i||this._printing)return;let s=ie();this._printing=!0;let n;try{n=await this._bookletData(t)}catch(h){throw s?.close(),h}finally{this._printing=!1}this._printOpen=!1;let c={title:a("service_record_title",e),generated:a("report_generated",e),manufacturer:a("manufacturer",e),model:a("model",e),serial:a("serial_number_label",e),installed:a("installed",e),colDate:a("date",e),colTask:a("task_name",e),colCost:a("cost",e),colDuration:a("duration",e),colNotes:a("notes_label",e),completedBy:a("completed_by",e),totalLabel:a("report_total_cost",e),entriesLabel:h=>`${h} ${a("service_record_entries",e)}`,capNote:a("object_history_cap_note",e),none:"\u2014",readings:a("print_inc_readings",e),parts:a("print_inc_parts",e),photos:a("print_inc_photos",e),documents:a("print_inc_documents",e),checklist:a("print_inc_checklist",e),refNumber:a("ref_number",e),scanHint:a("report_scan_hint",e),page:h=>a("search_page",e).replace("{page}",String(h))},r=t.map(h=>({...h,completedBy:h.completedBy?this.userName(h.completedBy):null,notes:h.notes?ne(h.notes,e):null})),u=Bi(i,r,c,h=>h?G(h,e):"",h=>Bt(h,e),h=>Q(h,this.currencySymbol,e),new Date().toISOString(),{capped:this._capped,options:{layout:this._printLayout,include:this._printInclude},data:{objectRef:ft(i),tasks:n.tasks,photos:n.photos,fmtNumber:h=>Y(h,e)}});qt(u,s)}_renderPrintOptions(t){let e=this._lang,i=this._printInclude,s=(c,r,u=!1)=>o` +`}var js=60,tt=class extends q{constructor(){super(...arguments);this.entryId="";this.object=null;this.tasks=[];this.currencySymbol="\u20AC";this.userName=()=>null;this._full={};this._loading=!1;this._filterTask="";this._from="";this._to="";this._expanded=!1;this._printOpen=!1;this._printLayout="chronological";this._printInclude={...fe};this._printing=!1;this._loadedFor=null;this._loadedSignature="";this._loadSeq=0;this._localeReady=!1}connectedCallback(){super.connectedCallback();try{let t=JSON.parse(Z(A.printOptions)||"null");(t?.layout==="by_task"||t?.layout==="chronological")&&(this._printLayout=t.layout),t?.include&&typeof t.include=="object"&&(this._printInclude={...fe,...t.include})}catch{}}_savePrintOptions(){K(A.printOptions,JSON.stringify({layout:this._printLayout,include:this._printInclude}))}_toggleInclude(t,e){this._printInclude={...this._printInclude,[t]:e},this._savePrintOptions()}get _lang(){return Y(this.hass)}updated(t){if(super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,at(this._lang).then(()=>this.requestUpdate())),this.entryId&&this._loadedFor!==this.entryId)this._loadedFor=this.entryId,this._full={},this._filterTask="",this._from="",this._to="",this._loadedSignature=this._historySignature(),this._loadFullHistories();else if(this.entryId&&t.has("tasks")){let e=this._historySignature();e!==this._loadedSignature&&(this._loadedSignature=e,this._loadFullHistories())}}_historySignature(){return JSON.stringify(this.tasks.map(t=>[t.id,t.history_count??null,t.history??[]]))}async _loadFullHistories(){let t=this.entryId,e=this.tasks,i=++this._loadSeq;if(!e.length){this._full={},this._loading=!1;return}this._loading=!0;let s=await Promise.all(e.map(async o=>{try{let c=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/history",entry_id:t,task_id:o.id});return[o.id,c.history??[]]}catch{return[o.id,o.history??[]]}}));this.entryId!==t||i!==this._loadSeq||(this._full=Object.fromEntries(s),this._loading=!1)}get _entries(){return ui(this.tasks.map(t=>({id:t.id,name:t.name,history:this._full[t.id]??t.history??[],ref_no:t.ref_no,reading_unit:t.reading_unit})))}get _capped(){return Object.values(this._full).some(t=>t.length>=hi)}_openTask(t){this.dispatchEvent(new CustomEvent("open-task",{detail:{taskId:t},bubbles:!0,composed:!0}))}async _bookletData(t){let e=this._printInclude,i=this._lang,s=[];if(e.documents||e.photos)try{s=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/documents/list",entry_id:this.entryId})).documents||[]}catch{s=[]}let o=new Map;e.qr&&this._printLayout==="by_task"&&await Promise.all(this.tasks.map(async u=>{try{let p=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/qr/generate",entry_id:this.entryId,task_id:u.id,url_mode:"server",action:"view"});p.svg_data_uri&&o.set(u.id,p.svg_data_uri)}catch{}}));let c=this.tasks.map(u=>({id:u.id,name:u.name,ref:xt(this.object,u),schedule:St(u,i)||null,documents:s.filter(p=>(p.task_ids||[]).includes(u.id)&&!(p.tags||[]).includes("photo")).map(p=>({title:rt(p),page:p.task_pages?.[u.id]??null,description:p.description||null})),qrDataUri:o.get(u.id)??null})),n={};if(e.photos){let u=[...new Set(t.filter(_=>_.type==="completed").flatMap(_=>_.photoIds))].slice(0,js),p=new Map(s.map(_=>[_.id,_]));await Promise.all(u.map(async _=>{let v=p.get(_),f=v&&rt(v)||_.slice(0,8);try{n[_]={name:f,url:new URL(await ie(this.hass,_),window.location.origin).href}}catch{n[_]={name:f,url:null}}}))}return{tasks:c,photos:n}}async _print(t){let e=this._lang,i=this.object;if(!i||this._printing)return;let s=ae();this._printing=!0;let o;try{o=await this._bookletData(t)}catch(p){throw s?.close(),p}finally{this._printing=!1}this._printOpen=!1;let c={title:a("service_record_title",e),generated:a("report_generated",e),manufacturer:a("manufacturer",e),model:a("model",e),serial:a("serial_number_label",e),installed:a("installed",e),colDate:a("date",e),colTask:a("task_name",e),colCost:a("cost",e),colDuration:a("duration",e),colNotes:a("notes_label",e),completedBy:a("completed_by",e),totalLabel:a("report_total_cost",e),entriesLabel:p=>`${p} ${a("service_record_entries",e)}`,capNote:a("object_history_cap_note",e),none:"\u2014",readings:a("print_inc_readings",e),parts:a("print_inc_parts",e),photos:a("print_inc_photos",e),documents:a("print_inc_documents",e),checklist:a("print_inc_checklist",e),refNumber:a("ref_number",e),scanHint:a("report_scan_hint",e),page:p=>a("search_page",e).replace("{page}",String(p))},n=t.map(p=>({...p,completedBy:p.completedBy?this.userName(p.completedBy):null,notes:p.notes?ce(p.notes,e):null})),u=Wi(i,n,c,p=>p?W(p,e):"",p=>qt(p,e),p=>U(p,this.currencySymbol,e),new Date().toISOString(),{capped:this._capped,options:{layout:this._printLayout,include:this._printInclude},data:{objectRef:yt(i),tasks:o.tasks,photos:o.photos,fmtNumber:p=>Q(p,e)}});Ut(u,s)}_renderPrintOptions(t){let e=this._lang,i=this._printInclude,s=(c,n,u=!1)=>r` `,n=this._printLayout==="by_task";return o` + @change=${p=>this._toggleInclude(c,p.target.checked)} /> + ${a(n,e)} + `,o=this._printLayout==="by_task";return r` `}render(){let t=this._lang,e=this._entries;if(!e.length&&!this._loading)return p;let i=ci(e,{taskId:this._filterTask||null,from:this._from||null,to:this._to||null}),{completed:s,totalCost:n}=oe(i),c=this._expanded?i:i.slice(0,15);return o` + `}render(){let t=this._lang,e=this._entries;if(!e.length&&!this._loading)return h;let i=gi(e,{taskId:this._filterTask||null,from:this._from||null,to:this._to||null}),{completed:s,totalCost:o}=le(i),c=this._expanded?i:i.slice(0,15);return r`

${a("object_history_section",t)} ${i.length} - ${this._loading?o`${a("loading",t)}`:p} + ${this._loading?r`${a("loading",t)}`:h} {this._printOpen=!this._printOpen}}> ${a("service_record_print",t)}

- ${this._printOpen?this._renderPrintOptions(i):p} + ${this._printOpen?this._renderPrintOptions(i):h}
- {this._filterTask=n.target.value}}> - ${this.tasks.map(r=>o``)} + ${this.tasks.map(n=>r``)} ${k(t.capNote)}

`:""} .lang=${t} .label=${a("date_from",t)} .value=${this._from} - @value-changed=${r=>{this._from=r.detail.value}} + @value-changed=${n=>{this._from=n.detail.value}} >
${k(t.capNote)}

`:""} .lang=${t} .label=${a("date_to",t)} .value=${this._to} - @value-changed=${r=>{this._to=r.detail.value}} + @value-changed=${n=>{this._to=n.detail.value}} >
- ${i.length===0?o`

${a("object_history_empty",t)}

`:o` + ${i.length===0?r`

${a("object_history_empty",t)}

`:r`
- ${c.map(r=>o` + ${c.map(n=>r`
- ${G(r.timestamp,t)} - ${a(r.type,t)} - + ${W(n.timestamp,t)} + ${a(n.type,t)} + - ${r.cost!=null?o`${Q(r.cost,this.currencySymbol,t)}`:p} - ${r.duration!=null?o`${Bt(r.duration,t)}`:p} + ${n.cost!=null?r`${U(n.cost,this.currencySymbol,t)}`:h} + ${n.partsCost!=null?r`${a(n.partsCounted?"history_parts_counted":"history_parts_info",t).replace("{amount}",U(n.partsCost,this.currencySymbol,t))}`:h} + ${n.purchaseCost!=null?r`${a("history_purchase_stock",t).replace("{amount}",U(n.purchaseCost,this.currencySymbol,t))}`:h} + ${n.duration!=null?r`${qt(n.duration,t)}`:h} - ${r.notes?o`${ne(r.notes,t)}`:p} + ${n.notes?r`${ce(n.notes,t)}`:h}
`)}
- ${i.length>c.length?o`{this._expanded=!0}}> + ${i.length>c.length?r`{this._expanded=!0}}> ${a("show_all",t)} (${i.length}) - `:p} + `:h}
${s} ${a("service_record_entries",t)} · ${a("report_total_cost",t)}: - ${Q(n,this.currencySymbol,t)} + ${U(o,this.currencySymbol,t)}
- ${this._capped?o`

${a("object_history_cap_note",t)}

`:p} + ${this._capped?r`

${a("object_history_cap_note",t)}

`:h} `}
`}};tt.styles=N` @@ -3200,7 +3252,7 @@ ${c.capped?`

${k(t.capNote)}

`:""} @media (max-width: 640px) { .notes { padding-left: 0; } } - `,g([R({attribute:!1})],tt.prototype,"hass",2),g([R()],tt.prototype,"entryId",2),g([R({attribute:!1})],tt.prototype,"object",2),g([R({attribute:!1})],tt.prototype,"tasks",2),g([R()],tt.prototype,"currencySymbol",2),g([R({attribute:!1})],tt.prototype,"userName",2),g([_()],tt.prototype,"_full",2),g([_()],tt.prototype,"_loading",2),g([_()],tt.prototype,"_filterTask",2),g([_()],tt.prototype,"_from",2),g([_()],tt.prototype,"_to",2),g([_()],tt.prototype,"_expanded",2),g([_()],tt.prototype,"_printOpen",2),g([_()],tt.prototype,"_printLayout",2),g([_()],tt.prototype,"_printInclude",2),g([_()],tt.prototype,"_printing",2);customElements.get("maintenance-object-history-section")||customElements.define("maintenance-object-history-section",tt);var lt=class lt extends q{constructor(){super(...arguments);this.flat=!1;this._ov=null;this._loading=!1;this._marking=!1;this._error="";this._history=null;this._rosterSort=lt._storedSort();this._typeFilter=null;this._recorded=[];this._historyRequested=!1;this._localeReady=!1;this._markAll=async()=>{await this._mark(void 0)};this._repair=async()=>{await this._act({type:"maintenance_supporter/battery_fleet/setup",language:this._lang})};this._loadHistory=async t=>{let e=t.target.open;if(V(A.batteryRosterOpen,e?"1":"0"),!(!e||this._historyRequested)){this._historyRequested=!0;try{let i=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview_history"});this._history=i.series}catch{this._history=null}}}}get _lang(){return K(this.hass)}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(t){Dt(this,t),t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,at(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._error="";let t=await P(this,{type:"maintenance_supporter/battery_fleet/overview"},{busy:e=>{this._loading=e},onError:e=>{this._error=e}});t!==void 0&&(this._ov=t)}async _act(t,e){this._marking||(this._error="",await P(this,t,{busy:i=>{this._marking=i},reload:async()=>{e?.(),await this._load()},onError:i=>{this._error=i}}))}async _mark(t){await this._act({type:"maintenance_supporter/battery_fleet/mark_replaced",...t?{entity_ids:t}:{}})}async _setExcluded(t,e){await this._act({type:"maintenance_supporter/battery_fleet/set_excluded",entity_id:t,excluded:e})}async _addBattery(t){let e=t.detail?.value;e&&await this._act({type:"maintenance_supporter/battery_fleet/set_included",entity_id:e,included:!0})}async _setTrackSelf(t){await this._setFleetOption("set_track_self_charging",t.target.checked)}async _setDueWithoutSensor(t){await this._setFleetOption("set_due_without_sensor",t.target.checked)}async _setFleetOption(t,e){await this._act({type:`maintenance_supporter/battery_fleet/${t}`,enabled:e})}_rosterOpen(){return Z(A.batteryRosterOpen)!=="0"}_predictedTitle(t,e){if(t.forecast_overdue)return a("battery_fleet_forecast_overdue",e);let i=this._predictedDate(t.days_until??0);if(t.predicted_source==="trend")return a("battery_fleet_predicted_trend",e).replace("{date}",i).replace("{confidence}",a("cal_confidence_"+(t.prediction_confidence||"medium"),e));if(t.lifetime_months!=null&&t.lifetime_source){let s=a("lifetime_source_"+t.lifetime_source,e).replace("{n}",String(t.lifetime_samples??0));return a("battery_fleet_predicted_typical",e).replace("{date}",i).replace("{months}",String(t.lifetime_months)).replace("{type}",t.battery_type).replace("{source}",s)}return a("battery_fleet_predicted_on",e).replace("{date}",i)}_sparkline(t){let e=this._history?.[t.entity_id];if(!e||e.points.length<2)return p;let i=110,s=24,n=2,c=e.points[0][0],r=e.points[e.points.length-1][0],u=Date.now()/1e3,h=t.status!=="low"&&t.predicted_source==="trend"&&t.days_until!=null?u+t.days_until*86400:null,m=Math.max(r,h??r),v=E=>m===c?n:n+(E-c)/(m-c)*(i-2*n),b=E=>n+(1-Math.min(100,Math.max(0,E))/100)*(s-2*n),y=e.points.map(([E,x])=>`${O(v(E))},${O(b(x))}`).join(" "),M=e.points[e.points.length-1][1],j=O(b(e.threshold));return o`{await this._mark(void 0)};this._repair=async()=>{await this._act({type:"maintenance_supporter/battery_fleet/setup",language:this._lang})};this._loadHistory=async t=>{let e=t.target.open;if(K(A.batteryRosterOpen,e?"1":"0"),!(!e||this._historyRequested)){this._historyRequested=!0;try{let i=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview_history"});this._history=i.series}catch{this._history=null}}}}get _lang(){return Y(this.hass)}get _canWrite(){return At(this.hass?.user,this._access)}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(t){Dt(this,t),t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,at(this._lang).then(()=>this.requestUpdate()),de(this.hass).then(e=>{this._access=e.access}),this._ov===null&&!this._loading&&this._load())}async _load(){this._error="";let t=await P(this,{type:"maintenance_supporter/battery_fleet/overview"},{busy:e=>{this._loading=e},onError:e=>{this._error=e}});t!==void 0&&(this._ov=t)}async _act(t,e){this._marking||(this._error="",await P(this,t,{busy:i=>{this._marking=i},reload:async()=>{e?.(),await this._load()},onError:i=>{this._error=i}}))}async _mark(t){await this._act({type:"maintenance_supporter/battery_fleet/mark_replaced",...t?{entity_ids:t}:{}})}async _setExcluded(t,e){await this._act({type:"maintenance_supporter/battery_fleet/set_excluded",entity_id:t,excluded:e})}async _addBattery(t){let e=t.detail?.value;e&&await this._act({type:"maintenance_supporter/battery_fleet/set_included",entity_id:e,included:!0})}async _setTrackSelf(t){await this._setFleetOption("set_track_self_charging",t.target.checked)}async _setDueWithoutSensor(t){await this._setFleetOption("set_due_without_sensor",t.target.checked)}async _setFleetOption(t,e){await this._act({type:`maintenance_supporter/battery_fleet/${t}`,enabled:e})}_rosterOpen(){return Z(A.batteryRosterOpen)!=="0"}_predictedTitle(t,e){if(t.forecast_overdue)return a("battery_fleet_forecast_overdue",e);let i=this._predictedDate(t.days_until??0);if(t.predicted_source==="trend")return a("battery_fleet_predicted_trend",e).replace("{date}",i).replace("{confidence}",a("cal_confidence_"+(t.prediction_confidence||"medium"),e));if(t.lifetime_months!=null&&t.lifetime_source){let s=a("lifetime_source_"+t.lifetime_source,e).replace("{n}",String(t.lifetime_samples??0));return a("battery_fleet_predicted_typical",e).replace("{date}",i).replace("{months}",String(t.lifetime_months)).replace("{type}",t.battery_type).replace("{source}",s)}return a("battery_fleet_predicted_on",e).replace("{date}",i)}_sparkline(t){let e=this._history?.[t.entity_id];if(!e||e.points.length<2)return h;let i=110,s=24,o=2,c=e.points[0][0],n=e.points[e.points.length-1][0],u=Date.now()/1e3,p=t.status!=="low"&&t.predicted_source==="trend"&&t.days_until!=null?u+t.days_until*86400:null,_=Math.max(n,p??n),v=E=>_===c?o:o+(E-c)/(_-c)*(i-2*o),f=E=>o+(1-Math.min(100,Math.max(0,E))/100)*(s-2*o),y=e.points.map(([E,x])=>`${O(v(E))},${O(f(x))}`).join(" "),M=e.points[e.points.length-1][1],j=O(f(e.threshold));return r`${k(t.capNote)}

`:""} ${a("battery_fleet_sparkline_hint",this._lang)} - ${h!==null?o``:p} -
`}static _storedSort(){return Z(A.batteryRosterSort)==="name"?"name":"urgency"}_setSort(t){this._rosterSort=t,V(A.batteryRosterSort,t)}_sortedRoster(t){let e=this._typeFilter===null?t:t.filter(s=>s.battery_type===this._typeFilter);if(this._rosterSort==="name")return e;let i=s=>s.status==="low"?-1e3+(s.level??101)/101:s.days_until??1/0;return[...e].sort((s,n)=>i(s)-i(n)||s.device_name.localeCompare(n.device_name))}_predictedDate(t){return this._fmtDate(Date.now()+t*864e5)}_fmtDate(t){return G(wt(new Date(t)),this._lang)}_shoppingLine(t){return Object.entries(t).map(([e,i])=>o`
`}static _storedSort(){return Z(A.batteryRosterSort)==="name"?"name":"urgency"}_setSort(t){this._rosterSort=t,K(A.batteryRosterSort,t)}_sortedRoster(t){let e=this._typeFilter===null?t:t.filter(s=>s.battery_type===this._typeFilter);if(this._rosterSort==="name")return e;let i=s=>s.status==="low"?-1e3+(s.level??101)/101:s.days_until??1/0;return[...e].sort((s,o)=>i(s)-i(o)||s.device_name.localeCompare(o.device_name))}_predictedDate(t){return this._fmtDate(Date.now()+t*864e5)}_fmtDate(t){return W(kt(new Date(t)),this._lang)}_shoppingLine(t){return Object.entries(t).map(([e,i])=>r``)}_toggleTypeFilter(t){if(this._typeFilter=this._typeFilter===t?null:t,this._typeFilter!==null){let e=this.shadowRoot?.querySelector("details.bf-roster");e&&!e.open&&(e.open=!0)}}async _recordJump(t,e){await this._act({type:"maintenance_supporter/battery_fleet/record_replacement",entity_id:t,replaced_at:new Date(e.at*1e3).toISOString()},()=>{this._recorded=[...this._recorded,t]})}_levelBar(t){let e=t.level;if(e==null)return p;let i=t.low_threshold??20,s=e<=i?"bad":e<=i+20?"warn":"good";return o`
@@ -5917,24 +5994,24 @@ ${c.capped?`

${k(t.capNote)}

`:""} - ${e.map(s=>o` + ${e.map(s=>r` this._showObject(s.entry_id)}> - + - + @@ -5943,7 +6020,7 @@ ${c.capped?`

${k(t.capNote)}

`:""}
${this._objRef(this._getObject(s.entry_id)?.object)}${s.name} - ${s.low?o``:p} + ${s.low?r``:h} ${s.object_name||"\u2014"}${s.stock!==null?Xt(s.stock,s.unit,t):"\u2014"}${s.stock!==null?Nt(s.stock,s.unit,t):"\u2014"} ${s.reorder_threshold??"\u2014"}${s.cost!=null?Q(s.cost,i,t):"\u2014"}${s.cost!=null?s.package_size?`${U(s.cost,i,t)} / ${Nt(s.package_size,s.unit??void 0,t)}`:U(s.cost,i,t):"\u2014"} ${s.storage_location||"\u2014"} - ${s.consumers.length===0?"\u2014":s.consumers.map(n=>o` + ${s.consumers.length===0?"\u2014":s.consumers.map(o=>r` ${n.pooled?`${n.object_name} \xB7 `:""}${n.task_name??n.task_id} + class="part-consumer-chip${o.pooled?" pooled":""}" + title=${`${o.object_name??""}: ${o.task_name??o.task_id} (\xD7${o.quantity})`} + >${o.pooled?`${o.object_name} \xB7 `:""}${o.task_name??o.task_id} `)}
`} - `}_renderAllAreas(){let t=this._lang;return this._ensureAreaUi(),o` + `}_renderAllAreas(){let t=this._lang;return this._ensureAreaUi(),r`