From 7b5e46e702ce73e4079871789384666f5ab140cb Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Thu, 30 Jul 2026 23:59:38 +0000 Subject: [PATCH] 217 files --- .../maintenance_supporter/__init__.py | 101 +- .../de/maintenance_supporter.yaml | 95 ++ .../en/maintenance_supporter.yaml | 95 ++ .../es/maintenance_supporter.yaml | 92 ++ .../fr/maintenance_supporter.yaml | 85 ++ .../it/maintenance_supporter.yaml | 92 ++ .../nl/maintenance_supporter.yaml | 96 ++ .../assist_sentences/responses/cs.json | 42 + .../assist_sentences/responses/da.json | 42 + .../assist_sentences/responses/de.json | 42 + .../assist_sentences/responses/en.json | 42 + .../assist_sentences/responses/es.json | 42 + .../assist_sentences/responses/fi.json | 42 + .../assist_sentences/responses/fr.json | 42 + .../assist_sentences/responses/hi.json | 42 + .../assist_sentences/responses/hu.json | 42 + .../assist_sentences/responses/it.json | 42 + .../assist_sentences/responses/ja.json | 42 + .../assist_sentences/responses/ko.json | 42 + .../assist_sentences/responses/nb.json | 42 + .../assist_sentences/responses/nl.json | 42 + .../assist_sentences/responses/pl.json | 42 + .../assist_sentences/responses/pt-br.json | 42 + .../assist_sentences/responses/pt.json | 42 + .../assist_sentences/responses/ru.json | 42 + .../assist_sentences/responses/sv.json | 42 + .../assist_sentences/responses/tr.json | 42 + .../assist_sentences/responses/uk.json | 42 + .../assist_sentences/responses/zh.json | 42 + .../maintenance_supporter/button.py | 4 +- .../config_flow_options_global.py | 67 +- .../maintenance_supporter/const.py | 5 + .../maintenance_supporter/coordinator.py | 131 +- .../entity/triggers/counter.py | 3 + .../components/budget-section-card.ts | 80 +- .../components/complete-dialog.ts | 109 +- .../frontend-src/components/qr-dialog.ts | 8 +- .../frontend-src/components/settings-view.ts | 90 +- .../frontend-src/components/task-dialog.ts | 244 +++- .../components/task-quick-actions-dialog.ts | 6 +- .../frontend-src/dialog-mount.ts | 3 + .../frontend-src/esbuild.mjs | 8 + .../frontend-src/helpers/report.ts | 11 +- .../frontend-src/helpers/worksheet.ts | 10 +- .../frontend-src/locales/cs.json | 11 + .../frontend-src/locales/da.json | 11 + .../frontend-src/locales/de.json | 11 + .../frontend-src/locales/en.json | 11 + .../frontend-src/locales/es.json | 11 + .../frontend-src/locales/fi.json | 11 + .../frontend-src/locales/fr.json | 11 + .../frontend-src/locales/hi.json | 11 + .../frontend-src/locales/hu.json | 11 + .../frontend-src/locales/it.json | 11 + .../frontend-src/locales/ja.json | 11 + .../frontend-src/locales/ko.json | 11 + .../frontend-src/locales/nb.json | 11 + .../frontend-src/locales/nl.json | 11 + .../frontend-src/locales/pl.json | 11 + .../frontend-src/locales/pt-br.json | 11 + .../frontend-src/locales/pt.json | 11 + .../frontend-src/locales/ru.json | 11 + .../frontend-src/locales/sv.json | 11 + .../frontend-src/locales/tr.json | 11 + .../frontend-src/locales/uk.json | 11 + .../frontend-src/locales/zh.json | 11 + .../frontend-src/maintenance-calendar-card.ts | 4 +- .../frontend-src/maintenance-card-editor.ts | 76 ++ .../frontend-src/maintenance-card.ts | 164 ++- .../frontend-src/maintenance-panel.ts | 43 +- .../frontend-src/types.ts | 30 +- .../frontend/locales/cs.json | 11 + .../frontend/locales/da.json | 11 + .../frontend/locales/de.json | 11 + .../frontend/locales/en.json | 11 + .../frontend/locales/es.json | 11 + .../frontend/locales/fi.json | 11 + .../frontend/locales/fr.json | 11 + .../frontend/locales/hi.json | 11 + .../frontend/locales/hu.json | 11 + .../frontend/locales/it.json | 11 + .../frontend/locales/ja.json | 11 + .../frontend/locales/ko.json | 11 + .../frontend/locales/nb.json | 11 + .../frontend/locales/nl.json | 11 + .../frontend/locales/pl.json | 11 + .../frontend/locales/pt-br.json | 11 + .../frontend/locales/pt.json | 11 + .../frontend/locales/ru.json | 11 + .../frontend/locales/sv.json | 11 + .../frontend/locales/tr.json | 11 + .../frontend/locales/uk.json | 11 + .../frontend/locales/zh.json | 11 + .../frontend/maintenance-calendar-card.js | 69 +- .../frontend/maintenance-card.js | 535 +++++--- .../frontend/maintenance-panel.js | 851 +++++++----- .../frontend/maintenance-strategy-shim.js | 1 + ...M4T.js => budget-section-card-PAADD7WI.js} | 68 +- .../{chunk-PEGRBZWY.js => chunk-KRBXISES.js} | 1 + .../{chunk-X7OW5OLQ.js => chunk-RLJYCMAK.js} | 9 +- .../{chunk-Q7DQUCT7.js => chunk-TF44IC56.js} | 3 +- ...t-XJHMLPL4.js => dialog-mount-UUGV3XIE.js} | 321 +++-- ...VD3.js => groups-section-card-RVBRIGJU.js} | 3 +- ...R.js => vacation-section-card-PLHOVD6O.js} | 3 +- .../maintenance-dashboard-strategy.js | 5 +- .../helpers/notification_manager.py | 6 +- .../maintenance_supporter/helpers/parts.py | 49 +- .../maintenance_supporter/helpers/pause.py | 61 +- .../maintenance_supporter/helpers/sanitize.py | 28 +- .../helpers/settings_registry.py | 2 + .../helpers/task_fields.py | 7 + .../maintenance_supporter/intent.py | 418 ++++-- .../maintenance_supporter/manifest.json | 2 +- .../maintenance_supporter/parts_runtime.py | 82 +- .../maintenance_supporter/strings.json | 11 + .../maintenance_supporter/todo.py | 4 +- .../translations/cs.json | 11 + .../translations/da.json | 11 + .../translations/de.json | 11 + .../translations/en.json | 11 + .../translations/es.json | 11 + .../translations/fi.json | 11 + .../translations/fr.json | 11 + .../translations/hi.json | 11 + .../translations/hu.json | 11 + .../translations/it.json | 11 + .../translations/ja.json | 11 + .../translations/ko.json | 11 + .../translations/nb.json | 11 + .../translations/nl.json | 11 + .../translations/pl.json | 11 + .../translations/pt-BR.json | 11 + .../translations/pt.json | 11 + .../translations/ru.json | 11 + .../translations/sv.json | 11 + .../translations/tr.json | 11 + .../translations/uk.json | 11 + .../translations/zh-Hans.json | 11 + .../websocket/__init__.py | 20 + .../websocket/analysis.py | 4 +- .../websocket/dashboard.py | 100 +- .../websocket/integration_setups.py | 8 +- .../maintenance_supporter/websocket/io.py | 23 +- .../websocket/objects.py | 30 +- .../websocket/problem_sensors.py | 8 +- .../websocket/tasks_actions.py | 70 +- .../websocket/tasks_crud.py | 52 +- .../websocket/tasks_history.py | 2 +- .../websocket/tasks_lifecycle.py | 19 +- .../websocket/tasks_persist.py | 15 + .../maintenance_supporter/websocket/users.py | 2 +- custom_components/powercalc/__init__.py | 2 +- .../powercalc/analytics/analytics.py | 2 +- custom_components/powercalc/common.py | 21 +- custom_components/powercalc/config_flow.py | 6 +- .../configuration/config_entry_conversion.py | 42 +- .../powercalc/configuration/global_config.py | 2 +- .../powercalc/configuration/sensor_config.py | 4 +- custom_components/powercalc/device_binding.py | 2 +- custom_components/powercalc/diagnostics.py | 10 +- custom_components/powercalc/discovery.py | 15 +- custom_components/powercalc/errors.py | 5 +- .../flow_helper/flows/daily_energy.py | 2 +- .../flow_helper/flows/global_configuration.py | 2 +- .../powercalc/flow_helper/flows/library.py | 64 +- .../powercalc/flow_helper/flows/real_power.py | 2 +- .../powercalc/flow_helper/schema.py | 2 +- .../powercalc/flow_helper/strategy_form.py | 7 +- .../powercalc/group_include/filter.py | 2 +- custom_components/powercalc/helpers.py | 8 +- custom_components/powercalc/manifest.json | 2 +- custom_components/powercalc/migrate.py | 21 +- .../powercalc/power_profile/factory.py | 3 +- .../powercalc/power_profile/library.py | 12 +- .../power_profile/loader/composite.py | 3 +- .../powercalc/power_profile/loader/local.py | 2 +- .../power_profile/loader/protocol.py | 4 +- .../powercalc/power_profile/loader/remote.py | 98 +- .../powercalc/power_profile/power_profile.py | 8 +- .../power_profile/sub_profile_selector.py | 6 +- custom_components/powercalc/select.py | 6 +- custom_components/powercalc/sensor.py | 57 +- .../powercalc/sensors/daily_energy.py | 4 +- .../sensors/group/config_entry_utils.py | 3 +- .../powercalc/sensors/group/custom.py | 22 +- .../powercalc/sensors/group/subtract.py | 2 +- custom_components/powercalc/sensors/power.py | 31 +- .../powercalc/sensors/utility_meter.py | 10 +- .../powercalc/service/gui_configuration.py | 11 +- .../powercalc/strategy/composite.py | 38 +- .../powercalc/strategy/factory.py | 14 +- .../powercalc/strategy/linear.py | 2 +- custom_components/powercalc/strategy/lut.py | 12 +- .../powercalc/translations/cs.json | 1202 +++++++++++++++++ .../powercalc/translations/da.json | 1202 +++++++++++++++++ .../powercalc/translations/de.json | 358 +++-- .../powercalc/translations/en.json | 14 + .../powercalc/translations/es.json | 252 ++-- .../powercalc/translations/fi.json | 1202 +++++++++++++++++ .../powercalc/translations/fr.json | 856 ++++++------ .../powercalc/translations/hu.json | 1202 +++++++++++++++++ .../powercalc/translations/it.json | 532 ++++---- .../powercalc/translations/nb.json | 1202 +++++++++++++++++ .../powercalc/translations/nl.json | 248 ++-- .../powercalc/translations/pl.json | 302 +++-- .../powercalc/translations/pt-BR.json | 290 ++-- .../powercalc/translations/pt.json | 834 ++++++------ .../powercalc/translations/ro.json | 770 ++++++----- .../powercalc/translations/ru.json | 226 ++-- .../powercalc/translations/sk.json | 308 +++-- .../powercalc/translations/sv.json | 634 +++++---- .../powercalc/translations/zh-Hans.json | 1202 +++++++++++++++++ .../statistics-graph-chart-card.js | 2 +- .../statistics-graph-chart-card.js.gz | Bin 297129 -> 299949 bytes .../translations.js | 167 ++- .../translations.js.gz | Bin 578673 -> 590822 bytes zigbee2mqtt/state.json | 20 +- 217 files changed, 15978 insertions(+), 3912 deletions(-) create mode 100644 custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml create mode 100644 custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml create mode 100644 custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml create mode 100644 custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml create mode 100644 custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml create mode 100644 custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/cs.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/da.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/de.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/en.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/es.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/fi.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/fr.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/hi.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/hu.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/it.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/ja.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/ko.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/nb.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/nl.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/pl.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/pt.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/ru.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/sv.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/tr.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/uk.json create mode 100644 custom_components/maintenance_supporter/assist_sentences/responses/zh.json rename custom_components/maintenance_supporter/frontend/strategy/chunks/{budget-section-card-L3FTZM4T.js => budget-section-card-PAADD7WI.js} (51%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-PEGRBZWY.js => chunk-KRBXISES.js} (94%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-X7OW5OLQ.js => chunk-RLJYCMAK.js} (69%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-Q7DQUCT7.js => chunk-TF44IC56.js} (95%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{dialog-mount-XJHMLPL4.js => dialog-mount-UUGV3XIE.js} (71%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{groups-section-card-TAA36VD3.js => groups-section-card-RVBRIGJU.js} (97%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{vacation-section-card-3SOHKD7R.js => vacation-section-card-PLHOVD6O.js} (97%) create mode 100644 custom_components/powercalc/translations/cs.json create mode 100644 custom_components/powercalc/translations/da.json create mode 100644 custom_components/powercalc/translations/fi.json create mode 100644 custom_components/powercalc/translations/hu.json create mode 100644 custom_components/powercalc/translations/nb.json create mode 100644 custom_components/powercalc/translations/zh-Hans.json diff --git a/custom_components/maintenance_supporter/__init__.py b/custom_components/maintenance_supporter/__init__.py index 814c898..a2d621d 100644 --- a/custom_components/maintenance_supporter/__init__.py +++ b/custom_components/maintenance_supporter/__init__.py @@ -47,6 +47,7 @@ from .const import ( CONF_BUDGET_MONTHLY, CONF_BUDGET_YEARLY, CONF_GROUPS, + CONF_INSTALL_ASSIST_SENTENCES, CONF_OBJECT, CONF_PANEL_ENABLED, CONF_TASKS, @@ -58,6 +59,11 @@ from .const import ( GLOBAL_UNIQUE_ID, MAX_COST, MAX_DURATION_MINUTES, + MAX_LABEL_LENGTH, + MAX_LABELS, + MAX_NAME_LENGTH, + MAX_TEXT_LENGTH, + MAX_TYPE_LENGTH, PLATFORMS, SERVICE_ADD_OBJECT, SERVICE_ADD_TASK, @@ -81,10 +87,16 @@ from .const import ( from .coordinator import MaintenanceCoordinator from .entity.summary_coordinator import MaintenanceSummaryCoordinator from .frontend import async_register_card +from .helpers.assist_sentences import async_sync as async_sync_assist_sentences from .helpers.dates import INTERVAL_UNITS from .helpers.documents import DocumentStore from .helpers.notification_manager import NotificationManager from .helpers.schedule import normalize_task_storage +from .helpers.task_fields import ( + INTERVAL_DAYS_RANGE, + TASK_PRIORITIES, + WARNING_DAYS_RANGE, +) from .panel import async_register_panel, async_unregister_panel from .storage import MaintenanceStore, async_migrate_to_store from .websocket import async_register_commands @@ -158,21 +170,29 @@ SERVICE_ADD_OBJECT_SCHEMA = vol.Schema( } ) +# The service task schemas mirror the WS `task/create` / `task/update` schemas +# field-for-field, consuming the SAME constants + ranges (const.py / +# helpers.task_fields) rather than restating literals — the service path is +# 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. SERVICE_ADD_TASK_SCHEMA = vol.Schema( { vol.Required("entry_id"): cv.string, - vol.Required("name"): vol.All(cv.string, vol.Length(min=1, max=255)), - vol.Optional("task_type"): cv.string, - vol.Optional("schedule_type"): cv.string, - vol.Optional("interval_days"): vol.All(vol.Coerce(int), vol.Range(min=1)), + 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("interval_days"): vol.All( + vol.Coerce(int), vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1]) + ), vol.Optional("interval_unit"): vol.In(INTERVAL_UNITS), vol.Optional("due_date"): cv.string, # Nested recurrence for the calendar kinds (weekdays / nth_weekday / # day_of_month); takes precedence over the flat fields above. vol.Optional("schedule"): dict, - vol.Optional("warning_days"): vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Optional("warning_days"): vol.All(vol.Coerce(int), vol.Range(min=WARNING_DAYS_RANGE[0], max=WARNING_DAYS_RANGE[1])), vol.Optional("enabled"): cv.boolean, - vol.Optional("notes"): cv.string, + vol.Optional("notes"): vol.All(cv.string, vol.Length(max=MAX_TEXT_LENGTH)), } ) @@ -180,18 +200,20 @@ SERVICE_UPDATE_TASK_SCHEMA = vol.Schema( { vol.Required("entry_id"): cv.string, vol.Required("task_id"): cv.string, - vol.Optional("name"): vol.All(cv.string, vol.Length(min=1, max=255)), - vol.Optional("task_type"): cv.string, - vol.Optional("schedule_type"): cv.string, - vol.Optional("interval_days"): vol.All(vol.Coerce(int), vol.Range(min=1)), + 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("interval_days"): vol.All( + vol.Coerce(int), vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1]) + ), vol.Optional("interval_unit"): vol.In(INTERVAL_UNITS), vol.Optional("due_date"): cv.string, vol.Optional("schedule"): dict, - vol.Optional("warning_days"): vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Optional("warning_days"): vol.All(vol.Coerce(int), vol.Range(min=WARNING_DAYS_RANGE[0], max=WARNING_DAYS_RANGE[1])), vol.Optional("enabled"): cv.boolean, - vol.Optional("notes"): cv.string, - vol.Optional("priority"): vol.In(["low", "normal", "high"]), - vol.Optional("labels"): [cv.string], + vol.Optional("notes"): vol.All(cv.string, vol.Length(max=MAX_TEXT_LENGTH)), + vol.Optional("priority"): vol.In(TASK_PRIORITIES), + vol.Optional("labels"): vol.All([vol.All(cv.string, vol.Length(max=MAX_LABEL_LENGTH))], vol.Length(max=MAX_LABELS)), } ) @@ -702,7 +724,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: try: if action_type == "complete": _LOGGER.info("Completing task %s via notification action", task_id) - await runtime_data.coordinator.complete_maintenance(task_id=task_id) + await runtime_data.coordinator.complete_maintenance(task_id=task_id, unattended=True) elif action_type == "skip": _LOGGER.info("Skipping task %s via notification action", task_id) await runtime_data.coordinator.skip_maintenance(task_id=task_id, reason="Skipped from notification") @@ -711,6 +733,17 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: if nm is not None: nm.snooze_task(entry_id, task_id) return # Snooze: keep notification visible for later reminder + except ServiceValidationError as err: + # The task demands details a notification button cannot capture. + # Not an error in our code — log it plainly and deliberately do + # NOT dismiss the notification, so the reminder survives until the + # user completes the task properly in the panel. + _LOGGER.warning( + "Notification completion of task %s rejected: %s", + task_id, + err, + ) + return except Exception: _LOGGER.exception( "Failed to handle notification action %s for task %s", @@ -755,11 +788,18 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: tag_id, ) user_id = event.context.user_id if event.context else None - await runtime_data.coordinator.complete_maintenance( - task_id=task_id, - completed_by=user_id, - notes="Completed via NFC tag", - ) + try: + await runtime_data.coordinator.complete_maintenance( + task_id=task_id, + completed_by=user_id, + notes="Completed via NFC tag", + unattended=True, + ) + except ServiceValidationError as err: + # A tag tap cannot capture a cost or a photo. Log it + # plainly and leave the task open so the user finishes it + # in the panel — the honest outcome, not a silent no-op. + _LOGGER.warning("NFC completion of task %s rejected: %s", task_id, err) return # No match found — not our tag, ignore silently @@ -1074,6 +1114,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf # Listen for options changes (panel toggle) entry.async_on_unload(entry.add_update_listener(_async_global_options_updated)) + # Keep /custom_sentences/ in step with the opt-in setting. Runs + # on every setup, not just on change, so an upgrade that ships new + # sentences reaches an install that already opted in. + await async_sync_assist_sentences( + hass, entry.options.get(CONF_INSTALL_ASSIST_SENTENCES, False) + ) + # Initial orphan check for admin_panel_user_ids (HA users deleted # while the integration was offline land here as repair issues). await _check_admin_panel_user_orphans(hass, entry) @@ -1305,6 +1352,10 @@ async def _async_global_options_updated(hass: HomeAssistant, entry: ConfigEntry) await _check_admin_panel_user_orphans(hass, entry) # The notify service may have just been (re)configured — re-check it. _verify_notify_service(hass) + # Install or remove the Assist sentence files to match the setting. + await async_sync_assist_sentences( + hass, entry.options.get(CONF_INSTALL_ASSIST_SENTENCES, False) + ) _ORPHAN_ISSUE_PREFIX = "orphan_admin_panel_user_" @@ -1466,6 +1517,16 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: # as a fixable repair issue so the user can restore it (#86). _sync_missing_global_entry_issue(hass) return + # #111: hand any shared spare-part pool to a borrower BEFORE the Store goes + # — the stock numbers exist nowhere else. Must happen here rather than in + # the panel's delete: HA's own Configure-UI removal never reaches that path. + from .helpers.shared_parts import async_transfer_pools_on_removal + + try: + await async_transfer_pools_on_removal(hass, entry) + except Exception: + _LOGGER.exception("Could not transfer shared spare parts from %s", entry.title) + store = hass.data.get(STORES_CACHE_KEY, {}).pop(entry.entry_id, None) if store is None: store = MaintenanceStore(hass, entry.entry_id) diff --git a/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml new file mode 100644 index 0000000..b94adc4 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/de/maintenance_supporter.yaml @@ -0,0 +1,95 @@ +# Assist-Sätze für Maintenance Supporter (Deutsch). +# +# 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. +# +# 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). +language: "de" +intents: + MaintenanceSupporterListTasks: + data: + - sentences: + - "welche Wartung[en] (ist|sind) [gerade] (fällig|überfällig)" + - "was ist [an Wartung] (fällig|überfä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). + - sentences: + - "was muss ich (machen|erledigen|tun)" + - "was sind meine (Aufgaben|Pflichten|Wartungen)" + - "(habe|hab) ich [noch] (was|etwas) zu (tun|erledigen)" + - "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" + - "(gibt es|steht) hier [drin] (etwas|was) an" + slots: + scope: "here" + 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)" + 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}" + MaintenanceSupporterTaskDue: + data: + - sentences: + - "wann ist [die] {task_name:name} fällig" + - "wann muss ich [die] {task_name:name} (machen|erledigen)" + MaintenanceSupporterSnoozeTask: + data: + - sentences: + - "[die] {task_name:name} (snoozen|stummschalten)" + - "Erinnerungen für [die] {task_name:name} (stummschalten|pausieren)" + MaintenanceSupporterPartStock: + data: + - sentences: + - "wie viele {part_name:name} haben wir [noch]" + - "wie ist der Bestand (von|an) {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" + - sentences: + - "verschiebe [die|das|den] {part_name:name} um eine Woche" + - "[die|das|den] {part_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" +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. + task_name: + wildcard: true + part_name: + wildcard: true + days: + range: + from: 1 + to: 365 diff --git a/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml new file mode 100644 index 0000000..e85295c --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/en/maintenance_supporter.yaml @@ -0,0 +1,95 @@ +# Assist sentences for Maintenance Supporter (English). +# +# You do not need to copy this by hand: turn on Settings -> General -> +# "Install Assist sentences" and the integration writes it to +# config/custom_sentences/en/maintenance_supporter.yaml and reloads the +# conversation agent. That is the only directory the classic (non-LLM) agent +# reads sentences from. LLM-based Assist pipelines need none of this — they +# pick the intents up as tools automatically. +# +# 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). +language: "en" +intents: + MaintenanceSupporterListTasks: + data: + - sentences: + - "what maintenance is (due|overdue)" + - "which maintenance [tasks] (are|is) (due|overdue|pending)" + - "is [there] any maintenance due" + - "what needs (maintenance|servicing)" + # 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: + - "what [maintenance] do I (need to|have to) do" + - "what (is|are) my (chores|tasks|maintenance [tasks])" + - "(is|are) [there] anything [assigned] for me [to do]" + - "what am I (due|responsible) for" + slots: + scope: "mine" + # Scope: the room the request came from, resolved from the satellite's + # area — so a wall tablet answers for where it is standing. + - sentences: + - "what needs (doing|maintenance|servicing) (in here|here)" + - "what maintenance is due (in here|here|in this room)" + - "is [there] anything [to do] (in here|in this room)" + slots: + scope: "here" + 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}" + MaintenanceSupporterTaskInstructions: + data: + - sentences: + - "how do I (do|perform) [the] {task_name:name}" + - "(instructions|guidance) 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}" + MaintenanceSupporterSnoozeTask: + data: + - sentences: + - "snooze [the] [maintenance] [task] {task_name:name}" + - "(mute|silence) [the] reminders for [the] {task_name:name}" + MaintenanceSupporterPartStock: + data: + - sentences: + - "how (many|much) {part_name:name} (do we have|are left|is left)" + - "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]" + - sentences: + - "postpone [the] {part_name:name} by a week" + - "push [the] {part_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}" +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. + task_name: + wildcard: true + part_name: + wildcard: true + days: + range: + from: 1 + to: 365 diff --git a/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml new file mode 100644 index 0000000..2c36aee --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/es/maintenance_supporter.yaml @@ -0,0 +1,92 @@ +# Frases de Assist para Maintenance Supporter (español). +# +# No hace falta copiar este archivo a mano: activa Ajustes -> General -> +# «Instalar las frases de Assist» y la integración lo escribe en +# config/custom_sentences/es/maintenance_supporter.yaml y recarga el agente de +# conversación. Ese es el único directorio del que el agente clásico (sin LLM) +# lee sus frases. Las canalizaciones de Assist basadas en un LLM no necesitan +# nada de esto: reciben las intenciones como herramientas. +# +# 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). +language: "es" +intents: + MaintenanceSupporterListTasks: + data: + - sentences: + - "qué mantenimiento (toca|hay que hacer|está pendiente)" + - "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é me toca [a mí] [hacer]" + - "qué tengo que hacer [yo]" + - "cuáles son mis (tareas|mantenimientos)" + - "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)" + - "hay algo pendiente (aquí|en esta habitación)" + slots: + scope: "here" + 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)" + 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}" + 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}" + MaintenanceSupporterSnoozeTask: + data: + - sentences: + - "(silencia|pausa) [el|la|los|las] {task_name:name}" + - "(silencia|pausa) los avisos (de|del|de la) {task_name:name}" + MaintenanceSupporterPartStock: + data: + - sentences: + - "cuántos {part_name:name} (nos quedan|tenemos|quedan)" + - "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]" + - sentences: + - "(aplaza|retrasa|pospón) [el|la|los|las] {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) esta (ronda|vuelta) (de|del|de la|de los|de las) {task_name:name}" +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». + task_name: + wildcard: true + part_name: + wildcard: true + days: + range: + from: 1 + to: 365 diff --git a/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml new file mode 100644 index 0000000..e50188b --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/fr/maintenance_supporter.yaml @@ -0,0 +1,85 @@ +# Phrases Assist pour Maintenance Supporter (français). +# +# Pas besoin de copier ce fichier à la main : activez Paramètres -> Général -> +# « Installer les phrases Assist » et l'intégration l'écrit dans +# config/custom_sentences/fr/maintenance_supporter.yaml puis recharge l'agent +# conversationnel. C'est le seul dossier où l'agent classique (non-LLM) lit ses +# phrases. Les pipelines Assist basées sur un LLM n'en ont pas besoin : elles +# récupèrent les intentions comme outils. +# +# 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). +language: "fr" +intents: + MaintenanceSupporterListTasks: + data: + - sentences: + - "quel[le]s (entretien|entretiens|maintenances|tâches) (est|sont) (dû|due|dues|prévus|prévues)" + - "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 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)" + slots: + scope: "here" + 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)" + MaintenanceSupporterTaskInstructions: + data: + - sentences: + - "comment (faire|je fais) [le|la|les|l'] {task_name:name}" + - "(instructions|consignes) pour [le|la|les|l'] {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}" + MaintenanceSupporterSnoozeTask: + data: + - sentences: + - "mets [le|la|les|l'] {task_name:name} en pause" + MaintenanceSupporterPartStock: + data: + - sentences: + - "combien de {part_name:name} (reste-t-il|il reste|avons-nous)" + 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]" + - sentences: + - "reporte [le|la|les|l'] {task_name:name} d'une semaine" + - "repousse [le|la|les|l'] {task_name:name} d'une semaine" + slots: + days: 7 + MaintenanceSupporterSkipTask: + data: + - sentences: + - "(saute|passe) [le|la|les|l'] {task_name:name} [cette fois]" +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 ». + task_name: + wildcard: true + part_name: + wildcard: true + days: + range: + from: 1 + to: 365 diff --git a/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml new file mode 100644 index 0000000..e5e4114 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/it/maintenance_supporter.yaml @@ -0,0 +1,92 @@ +# Frasi Assist per Maintenance Supporter (italiano). +# +# Non serve copiare questo file a mano: attiva Impostazioni -> Generale -> +# «Installa le frasi di Assist» e l'integrazione lo scrive in +# config/custom_sentences/it/maintenance_supporter.yaml e ricarica l'agente +# conversazionale. È l'unica cartella da cui l'agente classico (non LLM) legge +# le sue frasi. Le pipeline Assist basate su LLM non ne hanno bisogno: ricevono +# gli intent automaticamente come strumenti. +# +# 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). +language: "it" +intents: + MaintenanceSupporterListTasks: + data: + - sentences: + - "quale manutenzione (è in scadenza|serve|va fatta)" + - "quali manutenzioni (sono|ci sono) in scadenza" + - "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 devo fare [io]" + - "quali sono i miei (compiti|lavori|impegni)" + - "tocca a me [fare qualcosa]" + - "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)" + - "c'è qualcosa da fare (qui|in questa stanza)" + slots: + scope: "here" + 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)" + 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}" + 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}" + 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}" + MaintenanceSupporterPartStock: + data: + - sentences: + - "quanti {part_name:name} (abbiamo|ci sono|restano)" + - "quante scorte di {part_name:name} (abbiamo|ci sono)" + MaintenanceSupporterPostponeTask: + data: + - sentences: + - "(rimanda|rinvia|posticipa) [il|lo|la|l'|i|gli|le] {task_name:name} di {days} (giorno|giorni)" + - sentences: + - "(rimanda|rinvia|posticipa) [il|lo|la|l'|i|gli|le] {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 questo giro (di|del|della|dei|delle) {task_name:name}" +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». + task_name: + wildcard: true + part_name: + wildcard: true + days: + range: + from: 1 + to: 365 diff --git a/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml b/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml new file mode 100644 index 0000000..945edca --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/nl/maintenance_supporter.yaml @@ -0,0 +1,96 @@ +# Assist-zinnen voor Maintenance Supporter (Nederlands). +# +# Handmatig kopiëren hoeft niet: zet Instellingen -> Algemeen -> +# "Assist-zinnen installeren" aan, dan schrijft de integratie dit bestand naar +# config/custom_sentences/nl/maintenance_supporter.yaml en herlaadt de +# gespreksagent. Alleen uit die map leest de klassieke (niet-LLM) agent zijn +# zinnen. Op een LLM gebaseerde Assist-pijplijnen hebben dit niet nodig: die +# krijgen de intents automatisch als tools. +# +# 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). +language: "nl" +intents: + MaintenanceSupporterListTasks: + data: + - sentences: + - "welk onderhoud (staat open|moet er gebeuren|is er nodig)" + - "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 moet ik [nog] doen" + - "wat zijn mijn (taken|klusjes|klussen)" + - "ben ik aan de beurt" + - "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" + - "is hier [nog] (iets|wat) te doen" + slots: + scope: "here" + 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)" + 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}" + MaintenanceSupporterTaskDue: + data: + - sentences: + - "wanneer moet [de|het] {task_name:name} (gebeuren|gedaan worden)" + - "wanneer is [de|het] {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}" + 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}" + 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]" + - sentences: + - "stel [de|het] {task_name:name} een week uit" + - "schuif [de|het] {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" +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. + task_name: + wildcard: true + part_name: + wildcard: true + days: + range: + from: 1 + to: 365 diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/cs.json b/custom_components/maintenance_supporter/assist_sentences/responses/cs.json new file mode 100644 index 0000000..add017d --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/cs.json @@ -0,0 +1,42 @@ +{ + "none_due": "Všechno je v pořádku — žádná údržba nevyžaduje pozornost.", + "none_due_mine": "Tobě teď není přiřazeno nic.", + "none_due_here": "V této místnosti nevyžaduje údržbu nic.", + "unknown_user": "Nevím, kdo se ptá, takže nemůžu určit, které úkoly jsou tvoje.", + "unknown_area": "Nevím, ve které místnosti je toto zařízení — nejdřív ho přiřaď k oblasti.", + "tasks_due": "Pozornost vyžadují tyto úkoly údržby, celkem {count}: {items}.", + "task_due_one": "Pozornost vyžaduje jeden úkol údržby: {items}.", + "completed": "Úkol '{task}' u zařízení {object} je zapsaný jako hotový.", + "postponed": "Úkol '{task}' u zařízení {object} je odložený na {date}.", + "postpone_needs_when": "Řekni mi, o kolik ho mám odložit — třeba o pět dní.", + "postpone_past": "Tím by '{task}' spadl do minulosti.", + "skipped": "Tento cyklus úkolu '{task}' u zařízení {object} je přeskočený. Další termín je {date}.", + "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.", + "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", + "st_due_today": "termín dnes", + "st_due_in": "termín do {days} dní", + "st_due_in_one": "termín zítra", + "st_triggered": "spuštěno", + "item_on": "{task} — {object}", + "due_date_suffix": " Další termín je {date}.", + "guide_header": "Uložené informace k úkolu '{task}' u zařízení {object}: {segments}.", + "guide_none": "K úkolu '{task}' u zařízení {object} nejsou uložené žádné pokyny, dokumenty ani náhradní díly. Můžu místo toho dát obecné, neověřené rady — chceš?", + "guide_notes": "poznámky: {notes}", + "guide_checklist": "kroky kontrolního seznamu, celkem {count}: {steps}", + "guide_doc": "propojený dokument '{title}'", + "guide_doc_page": "propojený dokument '{title}', strana {page}", + "guide_url": "je uložený odkaz na dokumentaci", + "guide_part": "potřeba: {qty} × {part}{extras}", + "guide_part_loc": "místo uložení: {loc}", + "guide_part_stock": "na skladě: {stock}", + "snoozed": "Připomínky k úkolu '{task}' u zařízení {object} jsou ztlumené po dobu {hours} hodin.", + "snooze_unavailable": "Oznámení nejsou nastavená, takže není co ztlumit.", + "stock_line": "{stock} × {part} na skladě{loc}{low}.", + "stock_loc": " (místo uložení: {loc})", + "stock_low": " — na úrovni objednacího limitu nebo pod ním", + "stock_untracked": "{part} — u tohoto dílu sklad nesleduju.", + "part_not_found": "Nenašel jsem náhradní díl, který by odpovídal '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/da.json b/custom_components/maintenance_supporter/assist_sentences/responses/da.json new file mode 100644 index 0000000..1fc4596 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/da.json @@ -0,0 +1,42 @@ +{ + "none_due": "Alt er i orden — der er ingen vedligeholdelse, der kræver opmærksomhed.", + "none_due_mine": "Der er ikke noget tildelt dig lige nu.", + "none_due_here": "Der skal ikke laves vedligeholdelse i det her rum.", + "unknown_user": "Jeg ved ikke, hvem der spørger, så jeg kan ikke sige, hvilke opgaver der er dine.", + "unknown_area": "Jeg ved ikke, hvilket rum den her enhed står i — knyt den til et område først.", + "tasks_due": "{count} vedligeholdelsesopgaver kræver opmærksomhed: {items}.", + "task_due_one": "Én vedligeholdelsesopgave kræver opmærksomhed: {items}.", + "completed": "Jeg har markeret '{task}' på {object} som udført.", + "postponed": "Jeg har udskudt '{task}' på {object} til den {date}.", + "postpone_needs_when": "Sig, hvor langt jeg skal udskyde den — for eksempel fem dage.", + "postpone_past": "Så ville '{task}' ligge i fortiden.", + "skipped": "Jeg har sprunget denne omgang af '{task}' på {object} over. Næste omgang skal laves den {date}.", + "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.", + "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", + "st_due_today": "skal laves i dag", + "st_due_in": "skal laves om {days} dage", + "st_due_in_one": "skal laves i morgen", + "st_triggered": "udløst", + "item_on": "{task} på {object}", + "due_date_suffix": " Næste gang skal det laves den {date}.", + "guide_header": "Gemte oplysninger om '{task}' på {object}: {segments}.", + "guide_none": "Der er ikke gemt nogen vejledning, dokumenter eller reservedele til '{task}' på {object}. Jeg kan i stedet give generelle, ikke-bekræftede råd — vil du have det?", + "guide_notes": "noter: {notes}", + "guide_checklist": "{count} trin på tjeklisten: {steps}", + "guide_doc": "tilknyttet dokument '{title}'", + "guide_doc_page": "tilknyttet dokument '{title}', side {page}", + "guide_url": "der er gemt et link til dokumentation", + "guide_part": "{qty} × {part} skal bruges{extras}", + "guide_part_loc": "opbevaringssted {loc}", + "guide_part_stock": "{stock} på lager", + "snoozed": "Påmindelser for '{task}' på {object} er sat på pause i {hours} timer.", + "snooze_unavailable": "Notifikationer er ikke sat op, så der er ikke noget at sætte på pause.", + "stock_line": "{stock} × {part} på lager{loc}{low}.", + "stock_loc": " (opbevaringssted {loc})", + "stock_low": " — på eller under genbestillingsgrænsen", + "stock_untracked": "Der føres ikke lager for {part}.", + "part_not_found": "Jeg kunne ikke finde en reservedel, der passer på '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/de.json b/custom_components/maintenance_supporter/assist_sentences/responses/de.json new file mode 100644 index 0000000..2dfb852 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/de.json @@ -0,0 +1,42 @@ +{ + "none_due": "Alles in Ordnung — keine Wartung fällig.", + "none_due_mine": "Dir ist gerade nichts zugewiesen.", + "none_due_here": "In diesem Raum ist keine Wartung fällig.", + "unknown_user": "Ich weiß nicht, wer fragt, und kann deine Aufgaben deshalb nicht bestimmen.", + "unknown_area": "Ich weiß nicht, in welchem Raum dieses Gerät steht — ordne es zuerst einem Bereich zu.", + "tasks_due": "{count} Wartungsaufgaben brauchen Aufmerksamkeit: {items}.", + "task_due_one": "Eine Wartungsaufgabe braucht Aufmerksamkeit: {items}.", + "completed": "'{task}' an {object} als erledigt eingetragen.", + "postponed": "'{task}' an {object} auf den {date} verschoben.", + "postpone_needs_when": "Sag mir, um wie lange ich verschieben soll — zum Beispiel um fünf Tage.", + "postpone_past": "Damit läge '{task}' in der Vergangenheit.", + "skipped": "Dieser Durchgang von '{task}' an {object} wurde übersprungen. Der nächste ist am {date} fällig.", + "not_found": "Ich habe keine Wartungsaufgabe zu '{name}' gefunden.", + "ambiguous": "Das passt auf mehrere Aufgaben: {candidates}. Bitte formuliere genauer.", + "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", + "st_due_today": "heute fällig", + "st_due_in": "fällig in {days} Tagen", + "st_due_in_one": "morgen fällig", + "st_triggered": "ausgelöst", + "item_on": "{task} an {object}", + "due_date_suffix": " Der nächste Fälligkeitstermin ist der {date}.", + "guide_header": "Hinterlegte Informationen zu '{task}' an {object}: {segments}.", + "guide_none": "Zu '{task}' an {object} sind keine Anleitungen, Dokumente oder Ersatzteile hinterlegt. Ich kann stattdessen allgemeine, ungeprüfte Hinweise geben — möchtest du das?", + "guide_notes": "Notizen: {notes}", + "guide_checklist": "{count} Checklisten-Schritte: {steps}", + "guide_doc": "verknüpftes Dokument '{title}'", + "guide_doc_page": "verknüpftes Dokument '{title}', Seite {page}", + "guide_url": "ein Dokumentations-Link ist hinterlegt", + "guide_part": "{qty} × {part} benötigt{extras}", + "guide_part_loc": "Lagerort {loc}", + "guide_part_stock": "{stock} auf Lager", + "snoozed": "Erinnerungen für '{task}' an {object} für {hours} Stunden stummgeschaltet.", + "snooze_unavailable": "Benachrichtigungen sind nicht eingerichtet — es gibt nichts stummzuschalten.", + "stock_line": "{stock} × {part} auf Lager{loc}{low}.", + "stock_loc": " (Lagerort: {loc})", + "stock_low": " — an oder unter der Nachbestellgrenze", + "stock_untracked": "Für {part} wird kein Bestand geführt.", + "part_not_found": "Ich habe kein Ersatzteil zu '{name}' gefunden." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/en.json b/custom_components/maintenance_supporter/assist_sentences/responses/en.json new file mode 100644 index 0000000..ac80bd7 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/en.json @@ -0,0 +1,42 @@ +{ + "none_due": "Everything is OK — no maintenance needs attention.", + "none_due_mine": "Nothing is assigned to you right now.", + "none_due_here": "Nothing needs maintenance in this room.", + "unknown_user": "I don't know who is asking, so I can't tell which tasks are yours.", + "unknown_area": "I don't know which room this device is in — assign it to an area first.", + "tasks_due": "{count} maintenance tasks need attention: {items}.", + "task_due_one": "One maintenance task needs attention: {items}.", + "completed": "Completed '{task}' on {object}.", + "postponed": "Postponed '{task}' on {object} to {date}.", + "postpone_needs_when": "Tell me how far to postpone it — for example, by five days.", + "postpone_past": "That would put '{task}' in the past.", + "skipped": "Skipped this cycle of '{task}' on {object}. The next one is due {date}.", + "not_found": "I couldn't find a maintenance task matching '{name}'.", + "ambiguous": "That matches several tasks: {candidates}. Please be more specific.", + "too_early": "'{task}' can only be completed closer to its due date.", + "st_overdue": "{days} days overdue", + "st_overdue_one": "1 day overdue", + "st_due_today": "due today", + "st_due_in": "due in {days} days", + "st_due_in_one": "due tomorrow", + "st_triggered": "triggered", + "item_on": "{task} on {object}", + "due_date_suffix": " The next due date is {date}.", + "guide_header": "Stored guidance for '{task}' on {object}: {segments}.", + "guide_none": "There are no stored instructions, documents or spare parts for '{task}' on {object}. I can offer general, non-verified advice instead — would you like that?", + "guide_notes": "notes: {notes}", + "guide_checklist": "{count} checklist steps: {steps}", + "guide_doc": "linked document '{title}'", + "guide_doc_page": "linked document '{title}', page {page}", + "guide_url": "a documentation link is on file", + "guide_part": "{qty} × {part} needed{extras}", + "guide_part_loc": "stored at {loc}", + "guide_part_stock": "{stock} in stock", + "snoozed": "Snoozed reminders for '{task}' on {object} for {hours} hours.", + "snooze_unavailable": "Notifications aren't configured, so there is nothing to snooze.", + "stock_line": "{stock} × {part} in stock{loc}{low}.", + "stock_loc": " (stored at {loc})", + "stock_low": " — at or below the reorder threshold", + "stock_untracked": "Stock isn't tracked for {part}.", + "part_not_found": "I couldn't find a spare part matching '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/es.json b/custom_components/maintenance_supporter/assist_sentences/responses/es.json new file mode 100644 index 0000000..57e85df --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/es.json @@ -0,0 +1,42 @@ +{ + "none_due": "Todo en orden — no hay ningún mantenimiento pendiente.", + "none_due_mine": "Ahora mismo no tienes nada asignado.", + "none_due_here": "En esta habitación no hay ningún mantenimiento pendiente.", + "unknown_user": "No sé quién me está hablando, así que no puedo decirte qué tareas son tuyas.", + "unknown_area": "No sé en qué habitación está este dispositivo — asígnalo primero a un área.", + "tasks_due": "Hay {count} tareas de mantenimiento pendientes: {items}.", + "task_due_one": "Hay una tarea de mantenimiento pendiente: {items}.", + "completed": "Hecho. He anotado '{task}' en {object}.", + "postponed": "He aplazado '{task}' en {object} al {date}.", + "postpone_needs_when": "Dime cuánto quieres aplazarlo — por ejemplo, cinco días.", + "postpone_past": "Eso dejaría '{task}' en el pasado.", + "skipped": "Me he saltado este ciclo de '{task}' en {object}. El siguiente toca el {date}.", + "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.", + "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", + "st_due_today": "toca hoy", + "st_due_in": "toca en {days} días", + "st_due_in_one": "toca mañana", + "st_triggered": "se ha activado", + "item_on": "{task} en {object}", + "due_date_suffix": " La próxima fecha prevista es el {date}.", + "guide_header": "Esto es lo que hay guardado sobre '{task}' en {object}: {segments}.", + "guide_none": "No hay instrucciones, documentos ni recambios guardados para '{task}' en {object}. En su lugar puedo darte consejos generales, sin verificar. ¿Quieres que lo haga?", + "guide_notes": "notas: {notes}", + "guide_checklist": "{count} pasos de la lista: {steps}", + "guide_doc": "documento vinculado '{title}'", + "guide_doc_page": "documento vinculado '{title}', página {page}", + "guide_url": "hay guardado un enlace a la documentación", + "guide_part": "hacen falta {qty} × {part}{extras}", + "guide_part_loc": "guardado en {loc}", + "guide_part_stock": "{stock} en stock", + "snoozed": "He silenciado los avisos de '{task}' en {object} durante {hours} horas.", + "snooze_unavailable": "Las notificaciones no están configuradas, así que no hay nada que silenciar.", + "stock_line": "{stock} × {part} en stock{loc}{low}.", + "stock_loc": " (guardado en {loc})", + "stock_low": " — en el umbral de reposición o por debajo", + "stock_untracked": "De {part} no se lleva control de stock.", + "part_not_found": "No he encontrado ningún recambio que coincida con '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/fi.json b/custom_components/maintenance_supporter/assist_sentences/responses/fi.json new file mode 100644 index 0000000..11390ca --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/fi.json @@ -0,0 +1,42 @@ +{ + "none_due": "Kaikki on kunnossa — mikään huolto ei vaadi huomiota.", + "none_due_mine": "Sinulle ei ole juuri nyt osoitettu mitään.", + "none_due_here": "Tässä huoneessa ei ole mitään huollettavaa.", + "unknown_user": "En tiedä, kuka kysyy, joten en osaa kertoa, mitkä tehtävät ovat sinun.", + "unknown_area": "En tiedä, missä huoneessa tämä laite on — liitä se ensin alueeseen.", + "tasks_due": "{count} huoltotehtävää vaatii huomiota: {items}.", + "task_due_one": "Yksi huoltotehtävä vaatii huomiota: {items}.", + "completed": "Merkitsin tehtävän '{task}' tehdyksi kohteessa {object}.", + "postponed": "Siirsin tehtävää '{task}' kohteessa {object} myöhemmäksi. Uusi ajankohta: {date}.", + "postpone_needs_when": "Kerro, kuinka paljon siirrän sitä — esimerkiksi viisi päivää.", + "postpone_past": "Silloin tehtävä '{task}' menisi menneisyyteen.", + "skipped": "Ohitin tehtävän '{task}' tämän kerran kohteessa {object}. Seuraava ajankohta: {date}.", + "not_found": "En löytänyt huoltotehtävää, joka vastaisi hakua '{name}'.", + "ambiguous": "Tämä vastaa useaa tehtävää: {candidates}. Tarkenna vielä hieman.", + "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ä", + "st_due_today": "vuorossa tänään", + "st_due_in": "vuorossa {days} päivän päästä", + "st_due_in_one": "vuorossa huomenna", + "st_triggered": "laukesi", + "item_on": "{task} – kohteessa {object}", + "due_date_suffix": " Seuraava ajankohta on {date}.", + "guide_header": "Tallennetut tiedot tehtävälle '{task}' kohteessa {object}: {segments}.", + "guide_none": "Tehtävälle '{task}' kohteessa {object} ei ole tallennettu ohjeita, dokumentteja eikä varaosia. Voin antaa sen sijaan yleisiä, varmistamattomia neuvoja — sopiiko se sinulle?", + "guide_notes": "muistiinpanot: {notes}", + "guide_checklist": "{count} tarkistuslistan kohtaa: {steps}", + "guide_doc": "linkitetty dokumentti '{title}'", + "guide_doc_page": "linkitetty dokumentti '{title}', sivu {page}", + "guide_url": "tallennettuna on linkki dokumentaatioon", + "guide_part": "tarvitaan {qty} × {part}{extras}", + "guide_part_loc": "säilytyspaikka {loc}", + "guide_part_stock": "{stock} varastossa", + "snoozed": "Muistutukset tehtävästä '{task}' kohteessa {object} on vaimennettu {hours} tunniksi.", + "snooze_unavailable": "Ilmoituksia ei ole määritetty, joten vaimennettavaa ei ole.", + "stock_line": "{stock} × {part} varastossa{loc}{low}.", + "stock_loc": " (säilytyspaikka {loc})", + "stock_low": " — tilausrajalla tai sen alapuolella", + "stock_untracked": "Varastosaldoa ei seurata varaosalle {part}.", + "part_not_found": "En löytänyt varaosaa, joka vastaisi hakua '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/fr.json b/custom_components/maintenance_supporter/assist_sentences/responses/fr.json new file mode 100644 index 0000000..231572e --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/fr.json @@ -0,0 +1,42 @@ +{ + "none_due": "Tout va bien — il n'y a aucun entretien à faire.", + "none_due_mine": "Rien ne t'est attribué pour le moment.", + "none_due_here": "Il n'y a aucun entretien à faire dans cette pièce.", + "unknown_user": "Je ne sais pas qui me parle, donc je ne peux pas dire quelles tâches sont les tiennes.", + "unknown_area": "Je ne sais pas dans quelle pièce se trouve cet appareil — commence par l'affecter à une zone.", + "tasks_due": "{count} tâches d'entretien demandent ton attention : {items}.", + "task_due_one": "Une tâche d'entretien demande ton attention : {items}.", + "completed": "C'est fait pour '{task}' sur {object}.", + "postponed": "J'ai reporté '{task}' sur {object} au {date}.", + "postpone_needs_when": "Dis-moi de combien je dois le reporter — par exemple, de cinq jours.", + "postpone_past": "Ça mettrait '{task}' dans le passé.", + "skipped": "J'ai sauté ce cycle de '{task}' sur {object}. Le prochain est à faire le {date}.", + "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.", + "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", + "st_due_today": "à faire aujourd'hui", + "st_due_in": "à faire dans {days} jours", + "st_due_in_one": "à faire demain", + "st_triggered": "déclenché", + "item_on": "{task} sur {object}", + "due_date_suffix": " La prochaine échéance est le {date}.", + "guide_header": "Voilà ce qui est enregistré pour '{task}' sur {object} : {segments}.", + "guide_none": "Il n'y a ni instructions, ni documents, ni pièces détachées enregistrés pour '{task}' sur {object}. Je peux te donner des conseils généraux, non vérifiés — ça t'intéresse ?", + "guide_notes": "notes : {notes}", + "guide_checklist": "{count} étapes de la checklist : {steps}", + "guide_doc": "document lié '{title}'", + "guide_doc_page": "document lié '{title}', page {page}", + "guide_url": "un lien vers la documentation est enregistré", + "guide_part": "il faut {qty} × {part}{extras}", + "guide_part_loc": "rangé à {loc}", + "guide_part_stock": "{stock} en stock", + "snoozed": "J'ai mis les rappels de '{task}' sur {object} en pause pendant {hours} heures.", + "snooze_unavailable": "Les notifications ne sont pas configurées, il n'y a donc rien à mettre en pause.", + "stock_line": "{stock} × {part} en stock{loc}{low}.", + "stock_loc": " (rangé à {loc})", + "stock_low": " — au niveau ou en dessous du seuil de réapprovisionnement", + "stock_untracked": "Le stock n'est pas suivi pour {part}.", + "part_not_found": "Je n'ai pas trouvé de pièce détachée qui corresponde à '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/hi.json b/custom_components/maintenance_supporter/assist_sentences/responses/hi.json new file mode 100644 index 0000000..78299a0 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/hi.json @@ -0,0 +1,42 @@ +{ + "none_due": "सब ठीक है — किसी मेंटेनेंस पर ध्यान देने की ज़रूरत नहीं है।", + "none_due_mine": "अभी आपके नाम पर कुछ भी असाइन नहीं है।", + "none_due_here": "इस कमरे में किसी चीज़ की मेंटेनेंस बाकी नहीं है।", + "unknown_user": "मुझे नहीं पता कि कौन पूछ रहा है, इसलिए यह बताना मुश्किल है कि कौन से काम आपके हैं।", + "unknown_area": "मुझे नहीं पता कि यह डिवाइस किस कमरे में है — पहले इसे किसी एरिया में असाइन कर दीजिए।", + "tasks_due": "{count} मेंटेनेंस कामों पर ध्यान देना है: {items}।", + "task_due_one": "एक मेंटेनेंस काम पर ध्यान देना है: {items}।", + "completed": "{object} पर '{task}' को पूरा कर दिया है।", + "postponed": "{object} पर '{task}' को {date} तक टाल दिया है।", + "postpone_needs_when": "बताइए कितना टालना है — जैसे पाँच दिन।", + "postpone_past": "इससे '{task}' की तारीख़ पिछली हो जाएगी।", + "skipped": "{object} पर '{task}' का यह चक्र छोड़ दिया है। अगली बार {date} को करना है।", + "not_found": "'{name}' से मिलता-जुलता कोई मेंटेनेंस काम नहीं मिला।", + "ambiguous": "इससे कई काम मेल खाते हैं: {candidates}। थोड़ा और साफ़ बताइए।", + "too_early": "'{task}' को तय तारीख़ के क़रीब आने पर ही पूरा किया जा सकता है।", + "st_overdue": "{days} दिन की देरी", + "st_overdue_one": "एक दिन की देरी", + "st_due_today": "आज होना है", + "st_due_in": "{days} दिन में होना है", + "st_due_in_one": "कल होना है", + "st_triggered": "ट्रिगर हुआ", + "item_on": "{object} पर {task}", + "due_date_suffix": " अगली तारीख़ {date} है।", + "guide_header": "{object} पर '{task}' के लिए सहेजी गई जानकारी: {segments}।", + "guide_none": "{object} पर '{task}' के लिए कोई निर्देश, दस्तावेज़ या स्पेयर पार्ट सहेजा नहीं गया है। इसके बदले आम, बिना जाँची हुई सलाह दी जा सकती है — क्या आप चाहेंगे?", + "guide_notes": "नोट्स: {notes}", + "guide_checklist": "चेकलिस्ट के {count} चरण: {steps}", + "guide_doc": "जुड़ा हुआ दस्तावेज़ '{title}'", + "guide_doc_page": "जुड़ा हुआ दस्तावेज़ '{title}', पेज {page}", + "guide_url": "एक दस्तावेज़ लिंक सहेजा हुआ है", + "guide_part": "{qty} × {part} चाहिए{extras}", + "guide_part_loc": "रखने की जगह: {loc}", + "guide_part_stock": "स्टॉक में {stock}", + "snoozed": "{object} पर '{task}' के रिमाइंडर {hours} घंटे के लिए बंद कर दिए हैं।", + "snooze_unavailable": "नोटिफ़िकेशन सेट नहीं हैं, इसलिए बंद करने के लिए कुछ है ही नहीं।", + "stock_line": "स्टॉक: {stock} × {part}{loc}{low}।", + "stock_loc": " (रखने की जगह: {loc})", + "stock_low": " — दोबारा ऑर्डर करने की सीमा पर या उससे नीचे", + "stock_untracked": "{part} के लिए स्टॉक ट्रैक नहीं किया जाता।", + "part_not_found": "'{name}' से मिलता-जुलता कोई स्पेयर पार्ट नहीं मिला।" +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/hu.json b/custom_components/maintenance_supporter/assist_sentences/responses/hu.json new file mode 100644 index 0000000..6bc4ca1 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/hu.json @@ -0,0 +1,42 @@ +{ + "none_due": "Minden rendben — nincs esedékes karbantartás.", + "none_due_mine": "Jelenleg nincs hozzád rendelve semmi.", + "none_due_here": "Ebben a szobában nincs esedékes karbantartás.", + "unknown_user": "Nem tudom, ki kérdez, ezért nem tudom megmondani, melyik feladat a tiéd.", + "unknown_area": "Nem tudom, melyik szobában van ez az eszköz — rendeld hozzá előbb egy területhez.", + "tasks_due": "{count} karbantartási feladat esedékes: {items}.", + "task_due_one": "Egy karbantartási feladat esedékes: {items}.", + "completed": "Elvégzettnek jelöltem a feladatot: '{task}' – {object}.", + "postponed": "Elhalasztottam ezt a feladatot: '{task}' – {object}. Az új időpont: {date}.", + "postpone_needs_when": "Mondd meg, mennyivel halasszam el — például öt nappal.", + "postpone_past": "'{task}': ezzel a feladat a múltba kerülne.", + "skipped": "Kihagytam ezt a fordulót: '{task}' – {object}. A következő időpont: {date}.", + "not_found": "Nem találtam ehhez illő karbantartási feladatot: '{name}'.", + "ambiguous": "Több feladatra is illik: {candidates}. Fogalmazz pontosabban.", + "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", + "st_due_today": "ma esedékes", + "st_due_in": "{days} nap múlva esedékes", + "st_due_in_one": "holnap esedékes", + "st_triggered": "aktiválódott", + "item_on": "{task} – {object}", + "due_date_suffix": " A következő esedékesség: {date}.", + "guide_header": "'{task}' – {object}. A mentett információk: {segments}.", + "guide_none": "Nincsenek mentett útmutatók, dokumentumok vagy alkatrészek ehhez: '{task}' – {object}. Helyette tudok általános, nem ellenőrzött tanácsot adni — kéred?", + "guide_notes": "jegyzetek: {notes}", + "guide_checklist": "{count} lépés az ellenőrzőlistán: {steps}", + "guide_doc": "'{title}' című csatolt dokumentum", + "guide_doc_page": "'{title}' című csatolt dokumentum, {page}. oldal", + "guide_url": "el van mentve egy dokumentációs link", + "guide_part": "{qty} × {part} szükséges{extras}", + "guide_part_loc": "tárolási hely: {loc}", + "guide_part_stock": "{stock} van készleten", + "snoozed": "'{task}' – {object}: {hours} órára elnémítottam az emlékeztetőket.", + "snooze_unavailable": "Nincsenek beállítva értesítések, így nincs mit elnémítani.", + "stock_line": "{stock} × {part} van készleten{loc}{low}.", + "stock_loc": " (tárolási hely: {loc})", + "stock_low": " — az újrarendelési küszöbön vagy alatta", + "stock_untracked": "Ehhez nincs készletnyilvántartás: {part}.", + "part_not_found": "Nem találtam ehhez illő alkatrészt: '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/it.json b/custom_components/maintenance_supporter/assist_sentences/responses/it.json new file mode 100644 index 0000000..f941a51 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/it.json @@ -0,0 +1,42 @@ +{ + "none_due": "Tutto a posto — non c'è nessuna manutenzione da fare.", + "none_due_mine": "Al momento non hai niente assegnato.", + "none_due_here": "In questa stanza non c'è nessuna manutenzione da fare.", + "unknown_user": "Non so chi mi sta parlando, quindi non posso dirti quali attività sono tue.", + "unknown_area": "Non so in quale stanza si trova questo dispositivo — assegnalo prima a un'area.", + "tasks_due": "Ci sono {count} attività di manutenzione da fare: {items}.", + "task_due_one": "C'è un'attività di manutenzione da fare: {items}.", + "completed": "Fatto. Ho segnato '{task}' su {object}.", + "postponed": "Ho rimandato '{task}' su {object} al {date}.", + "postpone_needs_when": "Dimmi di quanto rimandarla — per esempio, di cinque giorni.", + "postpone_past": "Così '{task}' finirebbe nel passato.", + "skipped": "Ho saltato questo ciclo di '{task}' su {object}. Il prossimo è previsto per il {date}.", + "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?", + "too_early": "Puoi completare '{task}' solo quando si avvicina la scadenza.", + "st_overdue": "{days} giorni di ritardo", + "st_overdue_one": "un giorno di ritardo", + "st_due_today": "da fare oggi", + "st_due_in": "da fare tra {days} giorni", + "st_due_in_one": "da fare domani", + "st_triggered": "attivata", + "item_on": "{task} su {object}", + "due_date_suffix": " La prossima scadenza è il {date}.", + "guide_header": "Ecco cosa è salvato per '{task}' su {object}: {segments}.", + "guide_none": "Per '{task}' su {object} non ci sono istruzioni, documenti o ricambi salvati. Posso darti dei consigli generali, non verificati — ti va?", + "guide_notes": "note: {notes}", + "guide_checklist": "{count} passaggi della checklist: {steps}", + "guide_doc": "documento collegato '{title}'", + "guide_doc_page": "documento collegato '{title}', pagina {page}", + "guide_url": "è salvato un link alla documentazione", + "guide_part": "servono {qty} × {part}{extras}", + "guide_part_loc": "conservato in {loc}", + "guide_part_stock": "{stock} in magazzino", + "snoozed": "Ho messo in pausa i promemoria di '{task}' su {object} per {hours} ore.", + "snooze_unavailable": "Le notifiche non sono configurate, quindi non c'è niente da mettere in pausa.", + "stock_line": "{stock} × {part} in magazzino{loc}{low}.", + "stock_loc": " (conservato in {loc})", + "stock_low": " — pari o sotto la soglia di riordino", + "stock_untracked": "Per {part} non viene tenuta la giacenza.", + "part_not_found": "Non ho trovato nessun ricambio che corrisponda a '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ja.json b/custom_components/maintenance_supporter/assist_sentences/responses/ja.json new file mode 100644 index 0000000..519b1e8 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/ja.json @@ -0,0 +1,42 @@ +{ + "none_due": "すべて順調です。対応が必要なメンテナンスはありません。", + "none_due_mine": "今のところ、あなたに割り当てられているものはありません。", + "none_due_here": "この部屋にメンテナンスが必要なものはありません。", + "unknown_user": "どなたが話しているかわからないので、どれがあなたのタスクかお伝えできません。", + "unknown_area": "この機器がどの部屋にあるかわかりません。先にエリアを割り当ててください。", + "tasks_due": "対応が必要なメンテナンスが{count}件あります:{items}。", + "task_due_one": "対応が必要なメンテナンスが1件あります:{items}。", + "completed": "{object}の「{task}」を完了にしました。", + "postponed": "{object}の「{task}」を{date}に延期しました。", + "postpone_needs_when": "どのくらい先に延ばすか教えてください。たとえば「5日」のように言ってください。", + "postpone_past": "それだと「{task}」の日付が過去になってしまいます。", + "skipped": "{object}の「{task}」の今回分をスキップしました。次回は{date}が期限です。", + "not_found": "「{name}」に一致するメンテナンスタスクは見つかりませんでした。", + "ambiguous": "いくつかのタスクが当てはまります:{candidates}。もう少し詳しく言ってください。", + "too_early": "「{task}」は期限が近づいてからでないと完了にできません。", + "st_overdue": "{days}日遅れ", + "st_overdue_one": "1日遅れ", + "st_due_today": "今日が期限", + "st_due_in": "期限まであと{days}日", + "st_due_in_one": "明日が期限", + "st_triggered": "条件成立", + "item_on": "{object}の{task}", + "due_date_suffix": "次回の期限は{date}です。", + "guide_header": "{object}の「{task}」について、次の情報が保存されています:{segments}。", + "guide_none": "{object}の「{task}」には、手順も書類も交換部品も保存されていません。代わりに一般的な、確認済みではないアドバイスならお伝えできますが、いかがですか?", + "guide_notes": "メモ:{notes}", + "guide_checklist": "チェックリスト{count}項目:{steps}", + "guide_doc": "関連書類「{title}」", + "guide_doc_page": "関連書類「{title}」の{page}ページ", + "guide_url": "説明書のリンクが保存されています", + "guide_part": "{qty} × {part}が必要{extras}", + "guide_part_loc": "保管場所は{loc}", + "guide_part_stock": "在庫は{stock}", + "snoozed": "{object}の「{task}」のリマインダーを{hours}時間、一時停止しました。", + "snooze_unavailable": "通知が設定されていないので、一時停止するものがありません。", + "stock_line": "{stock} × {part}の在庫があります{loc}{low}。", + "stock_loc": "(保管場所は{loc})", + "stock_low": "。在庫は発注の目安以下です", + "stock_untracked": "{part}の在庫は管理していません。", + "part_not_found": "「{name}」に一致する交換部品は見つかりませんでした。" +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ko.json b/custom_components/maintenance_supporter/assist_sentences/responses/ko.json new file mode 100644 index 0000000..10a70b3 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/ko.json @@ -0,0 +1,42 @@ +{ + "none_due": "다 괜찮아요. 지금 챙길 정비는 없어요.", + "none_due_mine": "지금 배정된 작업은 없어요.", + "none_due_here": "이 방에는 정비가 필요한 게 없어요.", + "unknown_user": "누가 물어보는지 알 수 없어서, 어떤 작업이 배정돼 있는지 알려드릴 수 없어요.", + "unknown_area": "이 기기가 어느 방에 있는지 몰라요. 먼저 구역을 지정해 주세요.", + "tasks_due": "정비 작업 {count}개를 챙겨야 해요: {items}.", + "task_due_one": "정비 작업 하나를 챙겨야 해요: {items}.", + "completed": "{object}의 '{task}', 완료로 기록했어요.", + "postponed": "{object}의 '{task}', {date}까지 미뤘어요.", + "postpone_needs_when": "얼마나 미룰지 알려주세요. 예를 들어 '닷새 뒤'처럼요.", + "postpone_past": "그러면 '{task}' 날짜가 과거가 돼요.", + "skipped": "{object}의 '{task}', 이번 차례는 건너뛰었어요. 다음은 {date}에 예정돼 있어요.", + "not_found": "'{name}'에 맞는 정비 작업을 찾지 못했어요.", + "ambiguous": "여러 작업이 해당돼요: {candidates}. 조금 더 구체적으로 말씀해 주세요.", + "too_early": "'{task}' 작업은 예정일이 가까워져야 완료할 수 있어요.", + "st_overdue": "{days}일 지연", + "st_overdue_one": "하루 지연", + "st_due_today": "오늘 예정", + "st_due_in": "{days}일 뒤 예정", + "st_due_in_one": "내일 예정", + "st_triggered": "조건 충족", + "item_on": "{object}의 {task}", + "due_date_suffix": " 다음은 {date}에 예정돼 있어요.", + "guide_header": "{object}의 '{task}'에 저장된 정보예요: {segments}.", + "guide_none": "{object}의 '{task}'에는 저장된 안내나 문서, 예비 부품이 없어요. 대신 일반적인, 검증되지 않은 조언을 드릴 수 있는데, 들어보실래요?", + "guide_notes": "메모: {notes}", + "guide_checklist": "체크리스트 {count}단계: {steps}", + "guide_doc": "연결된 문서 '{title}'", + "guide_doc_page": "연결된 문서 '{title}' {page}쪽", + "guide_url": "설명서 링크가 저장돼 있어요", + "guide_part": "{qty} × {part} 필요{extras}", + "guide_part_loc": "보관 위치는 {loc}", + "guide_part_stock": "재고 {stock}", + "snoozed": "{object}의 '{task}' 알림을 {hours}시간 동안 미뤄뒀어요.", + "snooze_unavailable": "알림이 설정돼 있지 않아서 미뤄둘 게 없어요.", + "stock_line": "{stock} × {part} 재고가 있어요{loc}{low}.", + "stock_loc": " (보관 위치: {loc})", + "stock_low": ". 재주문 기준 이하예요", + "stock_untracked": "{part} 재고는 관리하지 않아요.", + "part_not_found": "'{name}'에 맞는 예비 부품을 찾지 못했어요." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/nb.json b/custom_components/maintenance_supporter/assist_sentences/responses/nb.json new file mode 100644 index 0000000..9792b9d --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/nb.json @@ -0,0 +1,42 @@ +{ + "none_due": "Alt er i orden — ingenting trenger vedlikehold nå.", + "none_due_mine": "Ingenting er tildelt deg akkurat nå.", + "none_due_here": "Ingenting trenger vedlikehold i dette rommet.", + "unknown_user": "Jeg vet ikke hvem som spør, så jeg kan ikke si hvilke oppgaver som er dine.", + "unknown_area": "Jeg vet ikke hvilket rom denne enheten står i — knytt den til et område først.", + "tasks_due": "{count} vedlikeholdsoppgaver trenger oppmerksomhet: {items}.", + "task_due_one": "Én vedlikeholdsoppgave trenger oppmerksomhet: {items}.", + "completed": "Jeg har krysset av '{task}' på {object}.", + "postponed": "Jeg har utsatt '{task}' på {object} til {date}.", + "postpone_needs_when": "Si hvor langt jeg skal utsette den — for eksempel fem dager.", + "postpone_past": "Da ville '{task}' havne i fortiden.", + "skipped": "Jeg hoppet over denne runden av '{task}' på {object}. Neste runde skal gjøres {date}.", + "not_found": "Jeg fant ingen vedlikeholdsoppgave som passer med '{name}'.", + "ambiguous": "Det passer med flere oppgaver: {candidates}. Vær litt mer presis.", + "too_early": "'{task}' kan først krysses av nærmere forfallsdatoen.", + "st_overdue": "{days} dager for sent", + "st_overdue_one": "én dag for sent", + "st_due_today": "skal gjøres i dag", + "st_due_in": "skal gjøres om {days} dager", + "st_due_in_one": "skal gjøres i morgen", + "st_triggered": "utløst", + "item_on": "{task} på {object}", + "due_date_suffix": " Neste forfallsdato er {date}.", + "guide_header": "Lagret informasjon om '{task}' på {object}: {segments}.", + "guide_none": "Det er ikke lagret noen instruksjoner, dokumenter eller reservedeler for '{task}' på {object}. Jeg kan i stedet gi generelle, ubekreftede råd — vil du det?", + "guide_notes": "notater: {notes}", + "guide_checklist": "{count} punkter på sjekklisten: {steps}", + "guide_doc": "tilknyttet dokument '{title}'", + "guide_doc_page": "tilknyttet dokument '{title}', side {page}", + "guide_url": "det er lagret en lenke til dokumentasjon", + "guide_part": "{qty} × {part} trengs{extras}", + "guide_part_loc": "oppbevaringssted {loc}", + "guide_part_stock": "{stock} på lager", + "snoozed": "Påminnelser for '{task}' på {object} er satt på pause i {hours} timer.", + "snooze_unavailable": "Varsler er ikke satt opp, så det er ingenting å sette på pause.", + "stock_line": "{stock} × {part} på lager{loc}{low}.", + "stock_loc": " (oppbevaringssted {loc})", + "stock_low": " — på eller under bestillingsgrensen", + "stock_untracked": "Det føres ikke lager for {part}.", + "part_not_found": "Jeg fant ingen reservedel som passer med '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/nl.json b/custom_components/maintenance_supporter/assist_sentences/responses/nl.json new file mode 100644 index 0000000..ce0ed0c --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/nl.json @@ -0,0 +1,42 @@ +{ + "none_due": "Alles in orde — er is geen onderhoud nodig.", + "none_due_mine": "Er staat op dit moment niets voor je open.", + "none_due_here": "In deze kamer is geen onderhoud nodig.", + "unknown_user": "Ik weet niet wie er vraagt, dus ik kan niet zeggen welke taken van jou zijn.", + "unknown_area": "Ik weet niet in welke kamer dit apparaat staat — wijs het eerst aan een gebied toe.", + "tasks_due": "Er zijn {count} onderhoudstaken die aandacht nodig hebben: {items}.", + "task_due_one": "Er is één onderhoudstaak die aandacht nodig heeft: {items}.", + "completed": "Gedaan. Ik heb '{task}' bij {object} afgevinkt.", + "postponed": "Ik heb '{task}' bij {object} uitgesteld naar {date}.", + "postpone_needs_when": "Zeg even hoe lang ik het moet uitstellen — bijvoorbeeld vijf dagen.", + "postpone_past": "Dan zou '{task}' in het verleden liggen.", + "skipped": "Ik heb deze ronde van '{task}' bij {object} overgeslagen. De volgende staat op {date}.", + "not_found": "Ik kon geen onderhoudstaak vinden die past bij '{name}'.", + "ambiguous": "Dat past bij meerdere taken: {candidates}. Kun je het iets preciezer zeggen?", + "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", + "st_due_today": "vandaag aan de beurt", + "st_due_in": "over {days} dagen aan de beurt", + "st_due_in_one": "morgen aan de beurt", + "st_triggered": "geactiveerd", + "item_on": "{task} bij {object}", + "due_date_suffix": " De volgende keer is op {date}.", + "guide_header": "Dit is er opgeslagen over '{task}' bij {object}: {segments}.", + "guide_none": "Er zijn geen instructies, documenten of onderdelen opgeslagen voor '{task}' bij {object}. Ik kan je wel algemene, niet-gecontroleerde tips geven — wil je dat?", + "guide_notes": "notities: {notes}", + "guide_checklist": "{count} stappen op de checklist: {steps}", + "guide_doc": "gekoppeld document '{title}'", + "guide_doc_page": "gekoppeld document '{title}', pagina {page}", + "guide_url": "er is een link naar de documentatie opgeslagen", + "guide_part": "je hebt {qty} × {part} nodig{extras}", + "guide_part_loc": "ligt in {loc}", + "guide_part_stock": "{stock} op voorraad", + "snoozed": "Ik heb de herinneringen voor '{task}' bij {object} {hours} uur gepauzeerd.", + "snooze_unavailable": "Meldingen zijn niet ingesteld, dus er valt niets te pauzeren.", + "stock_line": "{stock} × {part} op voorraad{loc}{low}.", + "stock_loc": " (ligt in {loc})", + "stock_low": " — op of onder de bestelgrens", + "stock_untracked": "Van {part} wordt de voorraad niet bijgehouden.", + "part_not_found": "Ik kon geen onderdeel vinden dat past bij '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pl.json b/custom_components/maintenance_supporter/assist_sentences/responses/pl.json new file mode 100644 index 0000000..352f122 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/pl.json @@ -0,0 +1,42 @@ +{ + "none_due": "Wszystko w porządku — żadna konserwacja nie wymaga uwagi.", + "none_due_mine": "W tej chwili nic nie jest do ciebie przypisane.", + "none_due_here": "W tym pomieszczeniu nic nie wymaga konserwacji.", + "unknown_user": "Nie wiem, kto pyta, więc nie mogę powiedzieć, które zadania są twoje.", + "unknown_area": "Nie wiem, w którym pomieszczeniu jest to urządzenie — najpierw przypisz je do obszaru.", + "tasks_due": "Zadania konserwacyjne wymagające uwagi, łącznie {count}: {items}.", + "task_due_one": "Jedno zadanie konserwacyjne wymaga uwagi: {items}.", + "completed": "Zadanie '{task}' na urządzeniu {object} zapisane jako wykonane.", + "postponed": "Zadanie '{task}' na urządzeniu {object} przełożone na {date}.", + "postpone_needs_when": "Powiedz mi, o ile mam przełożyć — na przykład o pięć dni.", + "postpone_past": "Wtedy '{task}' wypadłoby w przeszłości.", + "skipped": "Ten cykl zadania '{task}' na urządzeniu {object} został pominięty. Następny termin to {date}.", + "not_found": "Nie znalazłem zadania konserwacyjnego pasującego do '{name}'.", + "ambiguous": "To pasuje do kilku zadań: {candidates}. Doprecyzuj, proszę.", + "too_early": "Zadanie '{task}' można wykonać dopiero bliżej terminu.", + "st_overdue": "{days} dni po terminie", + "st_overdue_one": "jeden dzień po terminie", + "st_due_today": "termin dzisiaj", + "st_due_in": "termin za {days} dni", + "st_due_in_one": "termin jutro", + "st_triggered": "wyzwolone", + "item_on": "{task} — {object}", + "due_date_suffix": " Następny termin to {date}.", + "guide_header": "Zapisane informacje dla zadania '{task}' na urządzeniu {object}: {segments}.", + "guide_none": "Do zadania '{task}' na urządzeniu {object} nie ma zapisanych instrukcji, dokumentów ani części zamiennych. Mogę zamiast tego podać ogólne, niezweryfikowane wskazówki — chcesz?", + "guide_notes": "notatki: {notes}", + "guide_checklist": "kroki listy kontrolnej, łącznie {count}: {steps}", + "guide_doc": "powiązany dokument '{title}'", + "guide_doc_page": "powiązany dokument '{title}', strona {page}", + "guide_url": "zapisany jest link do dokumentacji", + "guide_part": "potrzebne: {qty} × {part}{extras}", + "guide_part_loc": "miejsce przechowywania: {loc}", + "guide_part_stock": "w magazynie: {stock}", + "snoozed": "Przypomnienia dla zadania '{task}' na urządzeniu {object} wyciszone przez okres {hours} godzin.", + "snooze_unavailable": "Powiadomienia nie są skonfigurowane, więc nie ma czego wyciszać.", + "stock_line": "{stock} × {part} w magazynie{loc}{low}.", + "stock_loc": " (miejsce przechowywania: {loc})", + "stock_low": " — na poziomie progu zamówienia lub poniżej", + "stock_untracked": "{part} — nie prowadzę stanu magazynowego dla tej części.", + "part_not_found": "Nie znalazłem części zamiennej pasującej do '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json b/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json new file mode 100644 index 0000000..be917a8 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json @@ -0,0 +1,42 @@ +{ + "none_due": "Está tudo em ordem — nenhuma manutenção precisa de atenção.", + "none_due_mine": "Nada está atribuído a você no momento.", + "none_due_here": "Nada precisa de manutenção neste cômodo.", + "unknown_user": "Não sei quem está perguntando, então não consigo dizer quais tarefas são suas.", + "unknown_area": "Não sei em que cômodo este dispositivo está — atribua ele a uma área primeiro.", + "tasks_due": "{count} tarefas de manutenção precisam de atenção: {items}.", + "task_due_one": "Uma tarefa de manutenção precisa de atenção: {items}.", + "completed": "Marquei '{task}' em {object} como concluída.", + "postponed": "Adiei '{task}' em {object} para {date}.", + "postpone_needs_when": "Me diga por quanto tempo adiar — por exemplo, cinco dias.", + "postpone_past": "Isso colocaria '{task}' no passado.", + "skipped": "Pulei este ciclo de '{task}' em {object}. O próximo vence em {date}.", + "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.", + "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", + "st_due_today": "vence hoje", + "st_due_in": "vence em {days} dias", + "st_due_in_one": "vence amanhã", + "st_triggered": "acionada", + "item_on": "{task} em {object}", + "due_date_suffix": " O próximo vencimento é {date}.", + "guide_header": "Informações registradas sobre '{task}' em {object}: {segments}.", + "guide_none": "Não há instruções, documentos nem peças de reposição registrados para '{task}' em {object}. Posso dar dicas gerais e não verificadas — você quer?", + "guide_notes": "observações: {notes}", + "guide_checklist": "{count} passos da lista de verificação: {steps}", + "guide_doc": "documento vinculado '{title}'", + "guide_doc_page": "documento vinculado '{title}', página {page}", + "guide_url": "há um link de documentação registrado", + "guide_part": "peça necessária: {qty} × {part}{extras}", + "guide_part_loc": "local: {loc}", + "guide_part_stock": "{stock} em estoque", + "snoozed": "Silenciei os lembretes de '{task}' em {object} por {hours} horas.", + "snooze_unavailable": "As notificações não estão configuradas, então não há nada para silenciar.", + "stock_line": "{stock} × {part} em estoque{loc}{low}.", + "stock_loc": " (local: {loc})", + "stock_low": " — no limite de reposição ou abaixo dele", + "stock_untracked": "Não há controle de estoque para {part}.", + "part_not_found": "Não encontrei nenhuma peça de reposição que corresponda a '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pt.json b/custom_components/maintenance_supporter/assist_sentences/responses/pt.json new file mode 100644 index 0000000..f3f5bce --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/pt.json @@ -0,0 +1,42 @@ +{ + "none_due": "Está tudo bem — não há nenhuma manutenção pendente.", + "none_due_mine": "Neste momento não tens nada atribuído.", + "none_due_here": "Nesta divisão não há nenhuma manutenção pendente.", + "unknown_user": "Não sei quem está a falar, por isso não consigo dizer que tarefas são tuas.", + "unknown_area": "Não sei em que divisão está este dispositivo — atribui-o primeiro a uma área.", + "tasks_due": "Há {count} tarefas de manutenção a precisar de atenção: {items}.", + "task_due_one": "Há uma tarefa de manutenção a precisar de atenção: {items}.", + "completed": "Feito. Registei '{task}' em {object}.", + "postponed": "Adiei '{task}' em {object} para {date}.", + "postpone_needs_when": "Diz-me quanto tempo queres adiar — por exemplo, cinco dias.", + "postpone_past": "Assim '{task}' ficaria no passado.", + "skipped": "Saltei este ciclo de '{task}' em {object}. O próximo está previsto para {date}.", + "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?", + "too_early": "Só podes concluir '{task}' mais perto da data prevista.", + "st_overdue": "{days} dias de atraso", + "st_overdue_one": "um dia de atraso", + "st_due_today": "para fazer hoje", + "st_due_in": "para fazer daqui a {days} dias", + "st_due_in_one": "para fazer amanhã", + "st_triggered": "ativada", + "item_on": "{task} em {object}", + "due_date_suffix": " A próxima data prevista é {date}.", + "guide_header": "Isto é o que está guardado sobre '{task}' em {object}: {segments}.", + "guide_none": "Não há instruções, documentos nem peças guardadas para '{task}' em {object}. Em vez disso posso dar-te conselhos gerais, não verificados — queres?", + "guide_notes": "notas: {notes}", + "guide_checklist": "{count} passos da lista: {steps}", + "guide_doc": "documento ligado '{title}'", + "guide_doc_page": "documento ligado '{title}', página {page}", + "guide_url": "está guardado um link para a documentação", + "guide_part": "precisas de {qty} × {part}{extras}", + "guide_part_loc": "guardado em {loc}", + "guide_part_stock": "{stock} em stock", + "snoozed": "Silenciei os lembretes de '{task}' em {object} durante {hours} horas.", + "snooze_unavailable": "As notificações não estão configuradas, por isso não há nada para silenciar.", + "stock_line": "{stock} × {part} em stock{loc}{low}.", + "stock_loc": " (guardado em {loc})", + "stock_low": " — no limite de reposição ou abaixo", + "stock_untracked": "O stock de {part} não é acompanhado.", + "part_not_found": "Não encontrei nenhuma peça que corresponda a '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ru.json b/custom_components/maintenance_supporter/assist_sentences/responses/ru.json new file mode 100644 index 0000000..d1c2c33 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/ru.json @@ -0,0 +1,42 @@ +{ + "none_due": "Всё в порядке — обслуживание пока не требуется.", + "none_due_mine": "На тебя сейчас ничего не назначено.", + "none_due_here": "В этой комнате обслуживание не требуется.", + "unknown_user": "Я не знаю, кто спрашивает, поэтому не могу определить, какие задачи твои.", + "unknown_area": "Я не знаю, в какой комнате это устройство — сначала назначь ему зону.", + "tasks_due": "Внимания требуют задачи обслуживания, всего {count}: {items}.", + "task_due_one": "Внимания требует одна задача обслуживания: {items}.", + "completed": "Задача '{task}' у устройства {object} отмечена как выполненная.", + "postponed": "Задача '{task}' у устройства {object} перенесена на {date}.", + "postpone_needs_when": "Скажи, на сколько перенести — например, на пять дней.", + "postpone_past": "Тогда '{task}' окажется в прошлом.", + "skipped": "Этот цикл задачи '{task}' у устройства {object} пропущен. Следующий срок — {date}.", + "not_found": "Я не нашёл задачу обслуживания, подходящую под '{name}'.", + "ambiguous": "Под это подходит несколько задач: {candidates}. Уточни, пожалуйста.", + "too_early": "Задачу '{task}' можно выполнить только ближе к сроку.", + "st_overdue": "просрочка {days} дней", + "st_overdue_one": "просрочка один день", + "st_due_today": "срок сегодня", + "st_due_in": "срок в течение {days} дней", + "st_due_in_one": "срок завтра", + "st_triggered": "сработало", + "item_on": "{task} — {object}", + "due_date_suffix": " Следующий срок — {date}.", + "guide_header": "Сохранённые сведения о задаче '{task}' у устройства {object}: {segments}.", + "guide_none": "Для задачи '{task}' у устройства {object} не сохранено ни инструкций, ни документов, ни запчастей. Могу дать общие, непроверенные советы — хочешь?", + "guide_notes": "заметки: {notes}", + "guide_checklist": "шаги контрольного списка, всего {count}: {steps}", + "guide_doc": "связанный документ '{title}'", + "guide_doc_page": "связанный документ '{title}', страница {page}", + "guide_url": "сохранена ссылка на документацию", + "guide_part": "нужно: {qty} × {part}{extras}", + "guide_part_loc": "место хранения: {loc}", + "guide_part_stock": "на складе: {stock}", + "snoozed": "Напоминания по задаче '{task}' у устройства {object} не будут приходить в течение {hours} часов.", + "snooze_unavailable": "Уведомления не настроены, так что отключать нечего.", + "stock_line": "{stock} × {part} на складе{loc}{low}.", + "stock_loc": " (место хранения: {loc})", + "stock_low": " — на уровне порога дозаказа или ниже", + "stock_untracked": "{part} — запас по этой детали не отслеживается.", + "part_not_found": "Я не нашёл запчасть, подходящую под '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/sv.json b/custom_components/maintenance_supporter/assist_sentences/responses/sv.json new file mode 100644 index 0000000..43d262a --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/sv.json @@ -0,0 +1,42 @@ +{ + "none_due": "Allt är i sin ordning — ingen underhållsuppgift behöver åtgärdas.", + "none_due_mine": "Ingenting är tilldelat dig just nu.", + "none_due_here": "Inget underhåll behöver göras i det här rummet.", + "unknown_user": "Jag vet inte vem som frågar, så jag kan inte säga vilka uppgifter som är dina.", + "unknown_area": "Jag vet inte vilket rum den här enheten står i — koppla den till ett område först.", + "tasks_due": "{count} underhållsuppgifter behöver åtgärdas: {items}.", + "task_due_one": "En underhållsuppgift behöver åtgärdas: {items}.", + "completed": "Jag har bockat av '{task}' på {object}.", + "postponed": "Jag har skjutit upp '{task}' på {object} till den {date}.", + "postpone_needs_when": "Säg hur långt jag ska skjuta upp den — till exempel fem dagar.", + "postpone_past": "Då skulle '{task}' hamna i det förflutna.", + "skipped": "Jag har hoppat över den här omgången av '{task}' på {object}. Nästa omgång ska göras den {date}.", + "not_found": "Jag hittade ingen underhållsuppgift som matchar '{name}'.", + "ambiguous": "Det matchar flera uppgifter: {candidates}. Var lite mer specifik.", + "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", + "st_due_today": "ska göras idag", + "st_due_in": "ska göras om {days} dagar", + "st_due_in_one": "ska göras imorgon", + "st_triggered": "utlöst", + "item_on": "{task} på {object}", + "due_date_suffix": " Nästa gång är det dags den {date}.", + "guide_header": "Sparad information om '{task}' på {object}: {segments}.", + "guide_none": "Det finns ingen sparad instruktion, inga dokument och inga reservdelar för '{task}' på {object}. Jag kan ge allmänna, overifierade råd i stället — vill du det?", + "guide_notes": "anteckningar: {notes}", + "guide_checklist": "{count} steg i checklistan: {steps}", + "guide_doc": "länkat dokument '{title}'", + "guide_doc_page": "länkat dokument '{title}', sidan {page}", + "guide_url": "det finns en länk till dokumentation", + "guide_part": "{qty} × {part} behövs{extras}", + "guide_part_loc": "förvaringsplats {loc}", + "guide_part_stock": "{stock} i lager", + "snoozed": "Jag har pausat påminnelserna för '{task}' på {object} i {hours} timmar.", + "snooze_unavailable": "Aviseringar är inte inställda, så det finns inget att pausa.", + "stock_line": "{stock} × {part} i lager{loc}{low}.", + "stock_loc": " (förvaringsplats {loc})", + "stock_low": " — vid eller under beställningsgränsen", + "stock_untracked": "Lagersaldot följs inte för {part}.", + "part_not_found": "Jag hittade ingen reservdel som matchar '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/tr.json b/custom_components/maintenance_supporter/assist_sentences/responses/tr.json new file mode 100644 index 0000000..81b0c7f --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/tr.json @@ -0,0 +1,42 @@ +{ + "none_due": "Her şey yolunda — bakım bekleyen bir şey yok.", + "none_due_mine": "Şu anda sana atanmış bir şey yok.", + "none_due_here": "Bu odada bakım bekleyen bir şey yok.", + "unknown_user": "Kimin sorduğunu bilmiyorum, bu yüzden hangi görevlerin sana ait olduğunu söyleyemem.", + "unknown_area": "Bu cihazın hangi odada olduğunu bilmiyorum — önce onu bir alana ata.", + "tasks_due": "{count} bakım görevi ilgilenmeyi bekliyor: {items}.", + "task_due_one": "Bir bakım görevi ilgilenmeyi bekliyor: {items}.", + "completed": "{object} için '{task}' görevi tamamlandı.", + "postponed": "{object} için '{task}' görevi {date} tarihine ertelendi.", + "postpone_needs_when": "Ne kadar erteleyeceğimi söyle — örneğin beş gün.", + "postpone_past": "Bu durumda '{task}' görevi geçmişte kalır.", + "skipped": "{object} için '{task}' görevinin bu turu atlandı. Sıradaki {date} tarihinde.", + "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?", + "too_early": "'{task}' görevi ancak son tarihine yaklaşınca tamamlanabilir.", + "st_overdue": "{days} gün gecikmiş", + "st_overdue_one": "bir gün gecikmiş", + "st_due_today": "bugün yapılacak", + "st_due_in": "{days} gün sonra yapılacak", + "st_due_in_one": "yarın yapılacak", + "st_triggered": "tetiklendi", + "item_on": "{object} için {task}", + "due_date_suffix": " Sıradaki bakım tarihi: {date}.", + "guide_header": "{object} için '{task}' görevine ait kayıtlı bilgiler: {segments}.", + "guide_none": "{object} için '{task}' görevine ait kayıtlı bir talimat, belge ya da yedek parça yok. İstersen bunun yerine genel, doğrulanmamış tavsiyeler verebilirim — ister misin?", + "guide_notes": "notlar: {notes}", + "guide_checklist": "{count} kontrol listesi adımı: {steps}", + "guide_doc": "bağlı belge '{title}'", + "guide_doc_page": "bağlı belge '{title}', sayfa {page}", + "guide_url": "kayıtlı bir doküman bağlantısı var", + "guide_part": "{qty} × {part} gerekiyor{extras}", + "guide_part_loc": "saklama yeri: {loc}", + "guide_part_stock": "stokta {stock} adet", + "snoozed": "{object} için '{task}' görevinin hatırlatmaları {hours} saat susturuldu.", + "snooze_unavailable": "Bildirimler ayarlanmamış, o yüzden susturulacak bir şey yok.", + "stock_line": "Stokta {stock} × {part} var{loc}{low}.", + "stock_loc": " (saklama yeri: {loc})", + "stock_low": " — yeniden sipariş sınırında ya da altında", + "stock_untracked": "{part} için stok takibi yapılmıyor.", + "part_not_found": "'{name}' ile eşleşen bir yedek parça bulamadım." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/uk.json b/custom_components/maintenance_supporter/assist_sentences/responses/uk.json new file mode 100644 index 0000000..4229715 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/uk.json @@ -0,0 +1,42 @@ +{ + "none_due": "Усе гаразд — обслуговування наразі не потрібне.", + "none_due_mine": "На тебе зараз нічого не призначено.", + "none_due_here": "У цій кімнаті обслуговування не потрібне.", + "unknown_user": "Я не знаю, хто питає, тому не можу визначити, які завдання твої.", + "unknown_area": "Я не знаю, у якій кімнаті цей пристрій — спершу признач йому зону.", + "tasks_due": "Уваги потребують завдання з обслуговування, усього {count}: {items}.", + "task_due_one": "Уваги потребує одне завдання з обслуговування: {items}.", + "completed": "Завдання '{task}' на пристрої {object} позначено як виконане.", + "postponed": "Завдання '{task}' на пристрої {object} перенесено на {date}.", + "postpone_needs_when": "Скажи, на скільки перенести — наприклад, на п’ять днів.", + "postpone_past": "Тоді '{task}' опиниться в минулому.", + "skipped": "Цей цикл завдання '{task}' на пристрої {object} пропущено. Наступний термін — {date}.", + "not_found": "Я не знайшов завдання з обслуговування, яке відповідає '{name}'.", + "ambiguous": "Цьому відповідає кілька завдань: {candidates}. Уточни, будь ласка.", + "too_early": "Завдання '{task}' можна виконати лише ближче до терміну.", + "st_overdue": "прострочення {days} днів", + "st_overdue_one": "прострочення один день", + "st_due_today": "термін сьогодні", + "st_due_in": "термін протягом {days} днів", + "st_due_in_one": "термін завтра", + "st_triggered": "спрацювало", + "item_on": "{task} — {object}", + "due_date_suffix": " Наступний термін — {date}.", + "guide_header": "Збережені відомості про завдання '{task}' на пристрої {object}: {segments}.", + "guide_none": "Для завдання '{task}' на пристрої {object} не збережено ані інструкцій, ані документів, ані запчастин. Можу натомість дати загальні, неперевірені поради — хочеш?", + "guide_notes": "нотатки: {notes}", + "guide_checklist": "кроки контрольного списку, усього {count}: {steps}", + "guide_doc": "пов’язаний документ '{title}'", + "guide_doc_page": "пов’язаний документ '{title}', сторінка {page}", + "guide_url": "збережено посилання на документацію", + "guide_part": "потрібно: {qty} × {part}{extras}", + "guide_part_loc": "місце зберігання: {loc}", + "guide_part_stock": "на складі: {stock}", + "snoozed": "Нагадування щодо завдання '{task}' на пристрої {object} не надходитимуть протягом {hours} годин.", + "snooze_unavailable": "Сповіщення не налаштовані, тож вимикати нічого.", + "stock_line": "{stock} × {part} на складі{loc}{low}.", + "stock_loc": " (місце зберігання: {loc})", + "stock_low": " — на рівні порога дозамовлення або нижче", + "stock_untracked": "{part} — запас цієї деталі не відстежується.", + "part_not_found": "Я не знайшов запчастину, яка відповідає '{name}'." +} diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/zh.json b/custom_components/maintenance_supporter/assist_sentences/responses/zh.json new file mode 100644 index 0000000..c90a417 --- /dev/null +++ b/custom_components/maintenance_supporter/assist_sentences/responses/zh.json @@ -0,0 +1,42 @@ +{ + "none_due": "一切正常,没有需要处理的维护。", + "none_due_mine": "现在没有分配给你的任务。", + "none_due_here": "这个房间里没有需要维护的东西。", + "unknown_user": "我不知道是谁在问,所以没法判断哪些任务是你的。", + "unknown_area": "我不知道这个设备在哪个房间,请先把它分配到一个区域。", + "tasks_due": "有{count}项维护任务需要处理:{items}。", + "task_due_one": "有一项维护任务需要处理:{items}。", + "completed": "已完成{object}的“{task}”。", + "postponed": "已把{object}的“{task}”推迟到{date}。", + "postpone_needs_when": "告诉我要推迟多久,比如推迟五天。", + "postpone_past": "那样的话,“{task}”的日期就会落在过去了。", + "skipped": "已跳过{object}的“{task}”这一次。下一次在{date}到期。", + "not_found": "我没有找到和“{name}”对得上的维护任务。", + "ambiguous": "有好几项任务都对得上:{candidates}。请说得更具体一些。", + "too_early": "“{task}”要等到接近到期日才能完成。", + "st_overdue": "已逾期{days}天", + "st_overdue_one": "已逾期一天", + "st_due_today": "今天到期", + "st_due_in": "{days}天后到期", + "st_due_in_one": "明天到期", + "st_triggered": "已触发", + "item_on": "{object}的{task}", + "due_date_suffix": "下一个到期日是{date}。", + "guide_header": "关于{object}的“{task}”,保存的信息有:{segments}。", + "guide_none": "{object}的“{task}”没有保存任何操作说明、文档或备件。我可以给你一些通用的、未经核实的建议,需要吗?", + "guide_notes": "备注:{notes}", + "guide_checklist": "{count}个清单步骤:{steps}", + "guide_doc": "关联文档“{title}”", + "guide_doc_page": "关联文档“{title}”第{page}页", + "guide_url": "已保存一个说明书链接", + "guide_part": "需要 {qty} × {part}{extras}", + "guide_part_loc": "存放在{loc}", + "guide_part_stock": "库存{stock}", + "snoozed": "已把{object}的“{task}”提醒暂停{hours}小时。", + "snooze_unavailable": "还没有设置通知,所以没有可以暂停的提醒。", + "stock_line": "库存 {stock} × {part}{loc}{low}。", + "stock_loc": "(存放在{loc})", + "stock_low": ",已达到或低于补货下限", + "stock_untracked": "{part}的库存没有记录。", + "part_not_found": "我没有找到和“{name}”对得上的备件。" +} diff --git a/custom_components/maintenance_supporter/button.py b/custom_components/maintenance_supporter/button.py index 81167a6..772174c 100644 --- a/custom_components/maintenance_supporter/button.py +++ b/custom_components/maintenance_supporter/button.py @@ -115,7 +115,9 @@ class MaintenanceActionButton(MaintenanceEntity, ButtonEntity): if not self._task_data: raise HomeAssistantError(f"Task {self._task_id} no longer exists") if self._action == "complete": - await self.coordinator.complete_maintenance(self._task_id, notes="Completed from dashboard button") + await self.coordinator.complete_maintenance( + self._task_id, notes="Completed from dashboard button", unattended=True + ) elif self._action == "skip": await self.coordinator.skip_maintenance(self._task_id, reason="Skipped from dashboard button") elif self._action == "reset": diff --git a/custom_components/maintenance_supporter/config_flow_options_global.py b/custom_components/maintenance_supporter/config_flow_options_global.py index 56aa8e4..de4fc21 100644 --- a/custom_components/maintenance_supporter/config_flow_options_global.py +++ b/custom_components/maintenance_supporter/config_flow_options_global.py @@ -138,6 +138,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Das Format des Benachrichtigungsdienstes ist ungültig. Verwenden Sie 'notify.dienstname'.", "failed": "❌ Testbenachrichtigung konnte nicht gesendet werden. Bitte prüfen Sie Ihre Konfiguration.", "push_message": "🔧 Testbenachrichtigung — Ihre Benachrichtigungseinrichtung funktioniert!", + "user_no_device": "ℹ️ Diesem Nutzer ist kein Companion-Gerät zugeordnet — seine Erinnerungen gehen an den Haushalts-Dienst.", }, "nl": { "success": "✅ Testmelding verzonden — uw service werkt. Krijgt een specifiek apparaat niets, controleer dat apparaat in uw notify-groep (en het HA-logboek).", @@ -145,6 +146,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Het formaat van de meldingsservice is ongeldig. Gebruik 'notify.servicenaam'.", "failed": "❌ Testmelding kon niet worden verzonden. Controleer uw configuratie.", "push_message": "🔧 Testmelding — uw meldingsinstellingen werken!", + "user_no_device": "ℹ️ Aan deze gebruiker is geen Companion-apparaat gekoppeld — hun herinneringen gaan naar de huishoudelijke dienst.", }, "fr": { "success": "✅ Notification de test envoyée — votre service fonctionne. Si un appareil précis ne reçoit rien, vérifiez-le dans votre groupe notify (et les journaux HA).", @@ -152,6 +154,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Le format du service de notification est invalide. Utilisez 'notify.nom_du_service'.", "failed": "❌ Impossible d'envoyer la notification de test. Veuillez vérifier votre configuration.", "push_message": "🔧 Notification de test — votre configuration de notifications fonctionne !", + "user_no_device": "ℹ️ Aucun appareil Companion n'est lié à cet utilisateur — ses rappels partent vers le service du foyer.", }, "it": { "success": "✅ Notifica di test inviata — il servizio funziona. Se un dispositivo specifico non riceve nulla, controllalo nel tuo gruppo notify (e nei log di HA).", @@ -159,6 +162,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Il formato del servizio di notifica non è valido. Usa 'notify.nome_servizio'.", "failed": "❌ Impossibile inviare la notifica di test. Verifica la tua configurazione.", "push_message": "🔧 Notifica di test — la configurazione delle notifiche funziona!", + "user_no_device": "ℹ️ Nessun dispositivo Companion è collegato a questo utente — i suoi promemoria vanno al servizio della casa.", }, "es": { "success": "✅ Notificación de prueba enviada — tu servicio funciona. Si un dispositivo concreto no recibe nada, revísalo en tu grupo notify (y los registros de HA).", @@ -166,6 +170,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ El formato del servicio de notificación no es válido. Use 'notify.nombre_servicio'.", "failed": "❌ No se pudo enviar la notificación de prueba. Verifique su configuración.", "push_message": "🔧 Notificación de prueba — ¡su configuración de notificaciones funciona!", + "user_no_device": "ℹ️ Este usuario no tiene ningún dispositivo Companion vinculado — sus recordatorios van al servicio del hogar.", }, "en": { "success": "✅ Test notification sent — your service works. If a specific device gets nothing, check that device inside your notify group (and Home Assistant's logs).", @@ -173,6 +178,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ The notification service format is invalid. Use 'notify.service_name'.", "failed": "❌ Failed to send the test notification. Please verify your service configuration.", "push_message": "🔧 Test notification — your notification setup is working!", + "user_no_device": "ℹ️ No Companion device is linked to this user — their reminders go to the household service.", }, "ru": { "success": "✅ Тестовое уведомление отправлено — сервис работает. Если конкретное устройство ничего не получает, проверьте его в вашей notify-группе (и в журналах HA).", @@ -180,6 +186,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Неверный формат сервиса уведомлений. Используйте 'notify.имя_сервиса'.", "failed": "❌ Не удалось отправить тестовое уведомление. Проверьте настройки сервиса.", "push_message": "🔧 Тестовое уведомление — ваша система уведомлений работает!", + "user_no_device": "ℹ️ К этому пользователю не привязано устройство Companion — его напоминания уходят в общий сервис.", }, "uk": { "success": "✅ Тестове сповіщення надіслано — служба працює. Якщо певний пристрій нічого не отримує, перевірте його у вашій notify-групі (та в журналах HA).", @@ -187,6 +194,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Невірний формат служби сповіщень. Використовуйте 'notify.service_name'.", "failed": "❌ Не вдалося надіслати тестове сповіщення. Перевірте конфігурацію служби.", "push_message": "🔧 Тестове сповіщення — ваші сповіщення працюють!", + "user_no_device": "ℹ️ До цього користувача не прив'язано пристрій Companion — його нагадування надходять до загальної служби.", }, "pt": { "success": "✅ Notificação de teste enviada — o seu serviço funciona. Se um dispositivo específico não receber nada, verifique-o no seu grupo notify (e nos registos do HA).", @@ -194,6 +202,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Formato inválido do serviço de notificação. Use 'notify.nome_do_servico'.", "failed": "❌ Falha ao enviar a notificação de teste. Verifique a configuração do serviço.", "push_message": "🔧 Notificação de teste — as suas notificações estão a funcionar!", + "user_no_device": "ℹ️ Este utilizador não tem nenhum dispositivo Companion associado — os lembretes seguem para o serviço da casa.", }, "zh": { "success": "✅ 测试通知已发送 — 您的服务正常。如果某个设备未收到,请在您的 notify 群组中检查该设备(以及 HA 日志)。", @@ -201,6 +210,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ 通知服务格式无效。请使用 'notify.服务名称' 格式。", "failed": "❌ 测试通知发送失败。请验证您的服务配置。", "push_message": "🔧 测试通知 — 您的通知设置已生效!", + "user_no_device": "ℹ️ 该用户未关联 Companion 设备 — 其提醒将发送到家庭通知服务。", }, "pt-br": { "success": "✅ Notificação de teste enviada — seu serviço funciona. Se um dispositivo específico não receber nada, verifique esse dispositivo dentro do seu grupo de notificação (e os logs do Home Assistant).", @@ -208,6 +218,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ O formato do serviço de notificação é inválido. Use 'notify.nome_do_servico'.", "failed": "❌ Falha ao enviar a notificação de teste. Verifique a configuração do serviço.", "push_message": "🔧 Notificação de teste — sua configuração de notificações está funcionando!", + "user_no_device": "ℹ️ Este usuário não tem nenhum dispositivo Companion vinculado — os lembretes vão para o serviço da casa.", }, "hu": { "success": "✅ Tesztértesítés elküldve — a szolgáltatás működik. Ha egy adott eszközre nem érkezik semmi, ellenőrizze az eszközt az értesítési csoportban (és a Home Assistant naplóit).", @@ -215,6 +226,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Az értesítési szolgáltatás formátuma érvénytelen. Használja a 'notify.szolgaltatas_nev' formát.", "failed": "❌ A tesztértesítés küldése nem sikerült. Ellenőrizze a szolgáltatás beállításait.", "push_message": "🔧 Tesztértesítés — az értesítési rendszere működik!", + "user_no_device": "ℹ️ Ehhez a felhasználóhoz nincs Companion eszköz rendelve — az emlékeztetői a háztartási szolgáltatásra mennek.", }, "ko": { "success": "✅ 테스트 알림을 보냈습니다 — 서비스가 작동합니다. 특정 기기에 알림이 오지 않으면 알림 그룹 내 해당 기기와 Home Assistant 로그를 확인하세요.", @@ -222,6 +234,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ 알림 서비스 형식이 잘못되었습니다. 'notify.service_name' 형식을 사용하세요.", "failed": "❌ 테스트 알림 전송에 실패했습니다. 서비스 설정을 확인하세요.", "push_message": "🔧 테스트 알림 — 알림 설정이 정상 작동합니다!", + "user_no_device": "ℹ️ 이 사용자에게 연결된 Companion 기기가 없습니다 — 알림은 가정 서비스로 전송됩니다.", }, "tr": { "success": "✅ Test bildirimi gönderildi — servisiniz çalışıyor. Belirli bir cihaza bildirim gelmiyorsa bildirim grubunuzdaki o cihazı (ve Home Assistant günlüklerini) kontrol edin.", @@ -229,6 +242,7 @@ _TEST_NOTIFICATION_RESULTS: dict[str, dict[str, str]] = { "invalid_service": "❌ Bildirim servisi biçimi geçersiz. 'notify.servis_adi' kullanın.", "failed": "❌ Test bildirimi gönderilemedi. Servis yapılandırmanızı kontrol edin.", "push_message": "🔧 Test bildirimi — bildirim kurulumunuz çalışıyor!", + "user_no_device": "ℹ️ Bu kullanıcıya bağlı bir Companion cihazı yok — hatırlatıcıları ev servisine gider.", }, } @@ -240,14 +254,36 @@ def _get_test_result_text(hass: HomeAssistant, key: str) -> str: return texts.get(key, texts.get("failed", key)) -async def send_test_notification(hass: HomeAssistant, options: dict[str, Any]) -> str: - """Send a test notification using the configured notify service. +async def send_test_notification( + hass: HomeAssistant, + options: dict[str, Any], + user_id: str | None = None, +) -> str: + """Send a test notification, optionally to ONE household member. - Returns a result key ("success", "no_service", "invalid_service", "failed") - that callers map to localized text. Action buttons are included whenever - the corresponding action-feature toggles are enabled, so the rendered - notification matches the real layout users see for actual tasks. + Returns a result key ("success", "no_service", "invalid_service", + "failed", "user_no_device") that callers map to localized text. Action + buttons are included whenever the corresponding action-feature toggles are + enabled, so the rendered notification matches the real layout users see + for actual tasks. + + With ``user_id`` the target is resolved through the SAME lookup the real + per-task routing uses (``get_user_notify_services``). That is the whole + point of a per-user test: one that took its own path could report success + while real reminders still went somewhere else — which is exactly how the + wrong-service bug behind #75 stayed invisible. """ + if user_id: + from .helpers.notification_manager import get_user_notify_services + + user_services = await get_user_notify_services(hass, user_id) + if not user_services: + # Not a failure: this member simply has no Companion device, so + # their reminders fall back to the household service. Saying so is + # more useful than sending a test that proves nothing. + return "user_no_device" + return await _send_test_to(hass, options, user_services) + notify_service = str(options.get(CONF_NOTIFY_SERVICE, "")) if not notify_service: return "no_service" @@ -258,6 +294,15 @@ async def send_test_notification(hass: HomeAssistant, options: dict[str, Any]) - if error: return "invalid_service" + return await _send_test_to(hass, options, [normalized]) + + +async def _send_test_to( + hass: HomeAssistant, + options: dict[str, Any], + services: list[str], +) -> str: + """Send the test payload to every resolved service; "success" if any went.""" try: from .helpers.notification_manager import async_dispatch_notify @@ -279,11 +324,13 @@ async def send_test_notification(hass: HomeAssistant, options: dict[str, Any]) - test_actions.append({"action": "MS_TEST_SNOOZE", "title": "\U0001f4a4 Snooze"}) service_data["data"] = {"actions": test_actions} # Dual-path: legacy notify service OR notify entity (send_message). - if not await async_dispatch_notify(hass, normalized, service_data, blocking=True): - return "failed" - return "success" + sent_any = False + for service in services: + if await async_dispatch_notify(hass, service, service_data, blocking=True): + sent_any = True + return "success" if sent_any else "failed" except Exception: # noqa: BLE001 - any failure mode reports "failed" to the UI - _LOGGER.debug("Test notification failed for %s", notify_service, exc_info=True) + _LOGGER.debug("Test notification failed for %s", services, exc_info=True) return "failed" diff --git a/custom_components/maintenance_supporter/const.py b/custom_components/maintenance_supporter/const.py index 5706a01..ee0e52b 100644 --- a/custom_components/maintenance_supporter/const.py +++ b/custom_components/maintenance_supporter/const.py @@ -249,6 +249,11 @@ CONF_SNOOZE_DURATION_HOURS = "snooze_duration_hours" # v2.15.0: opt-in weekly digest — a single Monday-morning summary notification. CONF_WEEKLY_DIGEST_ENABLED = "weekly_digest_enabled" + +# v2.44: opt-in copy of the shipped Assist sentence files into +# /custom_sentences/. The classic conversation agent reads sentences +# from there and nowhere else, and the HACS release ZIP never carried them. +CONF_INSTALL_ASSIST_SENTENCES = "install_assist_sentences" CONF_WARRANTY_REMINDER_ENABLED = "warranty_reminder_enabled" CONF_WARRANTY_REMINDER_DAYS = "warranty_reminder_days" DEFAULT_WARRANTY_REMINDER_DAYS = 30 diff --git a/custom_components/maintenance_supporter/coordinator.py b/custom_components/maintenance_supporter/coordinator.py index 05ed877..4d743b0 100644 --- a/custom_components/maintenance_supporter/coordinator.py +++ b/custom_components/maintenance_supporter/coordinator.py @@ -5,11 +5,12 @@ from __future__ import annotations import logging import time from collections.abc import Mapping -from datetime import date, datetime, timedelta +from datetime import date, timedelta from typing import TYPE_CHECKING, Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.util import dt as dt_util if TYPE_CHECKING: @@ -49,6 +50,7 @@ from .const import ( ScheduleType, TriggerEntityState, ) +from .helpers.budget import compute_spend from .helpers.schedule import normalize_task_storage, read_legacy_fields from .models.maintenance_object import MaintenanceObject from .models.maintenance_task import MaintenanceTask @@ -786,53 +788,18 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): Stores the result in hass.data[DOMAIN][BUDGET_CACHE_KEY] so that every coordinator reads from the same cache instead of each one re-scanning all entries on every 5-minute refresh. + + The scan itself lives in :func:`helpers.budget.compute_spend`, shared + with the ``budget_status`` WS command — this method only owns the + *caching* around it. The two used to hold divergent copies of the same + loop, so the alert and the panel could report different spend. """ - now = dt_util.now() - monthly = 0.0 - yearly = 0.0 - - for ce in self.hass.config_entries.async_entries(DOMAIN): - if ce.unique_id == GLOBAL_UNIQUE_ID: - continue - - rd = getattr(ce, "runtime_data", None) - ce_store = getattr(rd, "store", None) if rd else None - - for tid in ce.data.get(CONF_TASKS, {}): - if ce_store is not None: - history = ce_store.get_history(tid) - else: - history = ce.data.get(CONF_TASKS, {}).get(tid, {}).get("history", []) - - for h_entry in history: - if h_entry.get("type") != "completed": - continue - cost = h_entry.get("cost") - if cost is None: - continue - try: - cost_val = float(cost) - except (ValueError, TypeError): - continue - ts = h_entry.get("timestamp", "") - try: - entry_dt = datetime.fromisoformat(ts) - except (ValueError, TypeError): - continue - # Ensure TZ-aware so year/month boundaries are evaluated - # in HA's local timezone (otherwise off-by-one near midnight). - if entry_dt.tzinfo is None: - entry_dt = entry_dt.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) - entry_dt = dt_util.as_local(entry_dt) - if entry_dt.year == now.year: - yearly += cost_val - if entry_dt.month == now.month: - monthly += cost_val + monthly, yearly = compute_spend(self.hass) self.hass.data.setdefault(DOMAIN, {})[BUDGET_CACHE_KEY] = { "monthly_spent": monthly, "yearly_spent": yearly, - "last_updated": now, + "last_updated": dt_util.now(), } async def _async_check_budget(self, task_results: dict[str, Any]) -> None: @@ -965,13 +932,63 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): restock_quantity: float | None = None, used_parts: list[dict[str, Any]] | None = None, auto: bool = False, + unattended: bool = False, ) -> None: - """Mark a task as completed and persist.""" + """Mark a task as completed and persist. + + ``unattended`` marks a surface that cannot ask a human for anything — + a button press, a to-do tick, an NFC tap, a notification button, a + voice command. Those paths attach a canned provenance note + ("Completed via NFC tag"), which must NOT be mistaken for the note a + task demands: the point of a required note is that somebody wrote it. + """ merged = self._get_merged_tasks_data() if task_id not in merged: _LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title) return + # Required completion details. Checked HERE — the one point every + # surface funnels through — so a task demanding a note cannot be + # closed out from a button, the to-do list, an NFC tag, a + # notification action, voice or a service call. + # + # Deliberately BEFORE the double-complete guard below: a rejected + # attempt must not stamp that guard, or the corrected completion the + # user makes seconds later (after filling in the dialog) would be + # silently swallowed as a duplicate. + # + # Automatic completions are exempt — a self-clearing problem sensor + # has nobody to ask, and a required photo would strand the task. + if not auto: + from .helpers.completion_requirements import ( + missing_completion_fields, + required_completion_fields, + ) + + if unattended: + # Nobody was asked, so nothing the caller attached counts as + # an answer — a canned "Completed from the To-do list" note is + # provenance, not the note the task demands. + missing = required_completion_fields(merged[task_id]) + else: + missing = missing_completion_fields( + merged[task_id], + notes=notes, + cost=cost, + duration=duration, + photo_doc_id=photo_doc_id, + completed_by=completed_by, + ) + if missing: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="completion_details_required", + translation_placeholders={ + "task_name": str(merged[task_id].get("name", task_id)), + "fields": ", ".join(missing), + }, + ) + # Household double-complete guard (journey M1): two people seeing the # same overdue task and both tapping Complete within seconds would # record two completions — duplicated history/cost and a DOUBLE @@ -1241,6 +1258,28 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._lifecycle_event_payload(task, task_id, reason=reason), ) + + async def async_refresh_now(self) -> None: + """Recompute immediately — for changes a person just made. + + ``async_request_refresh`` is debounced, and Home Assistant's default + window is **ten seconds** (``REQUEST_REFRESH_DEFAULT_COOLDOWN``). That + is right for trigger-driven churn — a noisy sensor must not recompute + the object on every state change — and wrong for a user action: any + second action within that window had its request coalesced to the end + of it, so the panel kept showing the old computed status for up to ten + seconds and the change looked like it had done nothing. + + Assigning a user and then postponing the task was the reported case + (#111 review, 2026-07-28), but nothing about it is specific to those + two: completing two tasks on one object in quick succession, or any + bulk action, hit the same window. + + The recompute covers one object's tasks, so doing it per user action is + cheap; the debounced path stays exactly as it was for triggers. + """ + await self.async_refresh() + async def _persist_and_signal_task_change( self, task_id: str, @@ -1261,7 +1300,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.hass, SIGNAL_TASK_RESET.format(entry_id=self.entry.entry_id, task_id=task_id), ) - await self.async_request_refresh() + await self.async_refresh_now() def _lifecycle_event_payload( self, @@ -1321,7 +1360,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): new_data = dict(self.entry.data) new_data[CONF_TASKS] = tasks_data self.hass.config_entries.async_update_entry(self.entry, data=new_data) - await self.async_request_refresh() + await self.async_refresh_now() async def async_persist_trigger_runtime( self, diff --git a/custom_components/maintenance_supporter/entity/triggers/counter.py b/custom_components/maintenance_supporter/entity/triggers/counter.py index b01640a..0db65e1 100644 --- a/custom_components/maintenance_supporter/entity/triggers/counter.py +++ b/custom_components/maintenance_supporter/entity/triggers/counter.py @@ -165,6 +165,9 @@ class CounterTrigger(BaseTrigger): # falls back to the RAW counter value — a 27,000 km odometer reads # as "27000/15000, overdue" (issue #102). Debounced upstream, and # the periodic refresh never writes baselines, so no loop. + # Debounced ON PURPOSE: a counter entity can change many times + # a minute, and this is the path the ten-second coalescing + # window exists for. User actions use async_refresh_now. await self._coordinator.async_request_refresh() def reset(self) -> None: diff --git a/custom_components/maintenance_supporter/frontend-src/components/budget-section-card.ts b/custom_components/maintenance_supporter/frontend-src/components/budget-section-card.ts index d271918..c339038 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/budget-section-card.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/budget-section-card.ts @@ -11,18 +11,14 @@ import { property, state } from "lit/decorators.js"; import { t, ensureLocale, DEFAULT_CURRENCY_SYMBOL } from "../styles"; import { describeWsError } from "../ws-errors"; import { sectionCardSharedStyles } from "./section-card-shared-styles"; -import type { HomeAssistant } from "../types"; - -interface BudgetStatus { - monthly_budget?: number; - monthly_spent?: number; - yearly_budget?: number; - yearly_spent?: number; - currency_symbol?: string; -} +import type { BudgetStatus, HomeAssistant } from "../types"; interface CardConfig { type: string; title?: string; } +/** Backend default for budget_alert_threshold (const.py / settings_registry). + * Only reached if an older core omits the field from budget_status. */ +const DEFAULT_ALERT_THRESHOLD_PCT = 80; + export class MaintenanceBudgetSectionCard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config: CardConfig = { type: "" }; @@ -109,10 +105,21 @@ export class MaintenanceBudgetSectionCard extends LitElement { return html`
${t("loading", L) || "Loading…"}
`; } const sym = s.currency_symbol || DEFAULT_CURRENCY_SYMBOL; - const mPct = s.monthly_budget ? Math.min(100, ((s.monthly_spent || 0) / s.monthly_budget) * 100) : 0; - const yPct = s.yearly_budget ? Math.min(100, ((s.yearly_spent || 0) / s.yearly_budget) * 100) : 0; - const mWarn = mPct >= 100 ? "danger" : mPct >= 80 ? "warning" : "ok"; - const yWarn = yPct >= 100 ? "danger" : yPct >= 80 ? "warning" : "ok"; + // The amber step is the CONFIGURED budget_alert_threshold, not a literal 80 + // — same rule the panel's budget bar uses (maintenance-panel._renderBudgetBar). + const threshold = s.alert_threshold_pct ?? DEFAULT_ALERT_THRESHOLD_PCT; + const tracks = [ + { + label: t("budget_monthly", L) || "Monthly", + spent: s.monthly_spent || 0, + budget: s.monthly_budget || 0, + }, + { + label: t("budget_yearly", L) || "Yearly", + spent: s.yearly_spent || 0, + budget: s.yearly_budget || 0, + }, + ]; return html` @@ -127,25 +134,34 @@ export class MaintenanceBudgetSectionCard extends LitElement { ${this._error ? html`
${this._error}
` : nothing} -
-
- - - ${(s.monthly_spent || 0).toFixed(0)} / ${(s.monthly_budget || 0).toFixed(0)} ${sym} - -
-
-
- -
-
- - - ${(s.yearly_spent || 0).toFixed(0)} / ${(s.yearly_budget || 0).toFixed(0)} ${sym} - -
-
-
+ ${tracks.map((track) => { + // #104: budget tracking without a maximum — a bar needs a + // denominator, so show the plain spent total instead of "9 / 0 €" + // over an always-empty bar. Mirrors the panel's spent-only lines. + if (!(track.budget > 0)) { + return html` +
+
+ + ${track.spent.toFixed(0)} ${sym} +
+
+ `; + } + const pct = Math.min(100, Math.max(0, (track.spent / track.budget) * 100)); + const warn = pct >= 100 ? "danger" : pct >= threshold ? "warning" : "ok"; + return html` +
+
+ + + ${track.spent.toFixed(0)} / ${track.budget.toFixed(0)} ${sym} + +
+
+
+ `; + })} ${this._isAdmin ? html` 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 29a0ad8..2896af2 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts @@ -2,9 +2,11 @@ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; -import type { HomeAssistant } from "../types"; +import type { HomeAssistant, TaskPartLink } from "../types"; import { t, nativeFieldStyles } from "../styles"; import { describeWsError } from "../ws-errors"; +import { partLinkKey, type LinkedPart } from "../helpers/shared-parts"; +import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels"; export class MaintenanceCompleteDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -19,12 +21,20 @@ export class MaintenanceCompleteDialog extends LitElement { @property() public readingUnit = ""; /** Buy task (part_ref): default restock quantity — shows an editable qty field. */ @property({ attribute: false }) public restockDefault: number | null = null; - /** #99: the object's parts — enables the editable "parts used" section. */ - @property({ attribute: false }) public parts: Array<{ id: string; name: string; unit?: string | null; stock?: number | null }> = []; - /** #99: the task's fixed consumes_parts links (prefill for the section). */ - @property({ attribute: false }) public consumesParts: Array<{ part_id: string; quantity: number }> = []; + /** #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. */ + @property({ attribute: false }) public parts: LinkedPart[] = []; + /** #99: the task's fixed consumes_parts links (prefill for the section). + * A link may carry an `entry_id` (#111) and MUST keep it through the edit — + * without it the completion would decrement the wrong inventory, or none. */ + @property({ attribute: false }) public consumesParts: TaskPartLink[] = []; /** "Consumes: 1× HEPA-Filter (Shelf B)" hint lines for consuming tasks. */ @property({ type: Array }) public consumesInfo: string[] = []; + /** Details this task demands before it counts as done (v2.44). The backend + * enforces the same list at every completion surface; blocking Save here + * just means the user never has to meet that rejection. */ + @property({ type: Array }) public requiredFields: string[] = []; @state() private _open = false; @state() private _notes = ""; @state() private _cost = ""; @@ -38,7 +48,9 @@ export class MaintenanceCompleteDialog extends LitElement { @state() private _photoUploading = false; @state() private _readingValue = ""; @state() private _restockQty = ""; - @state() private _usedParts: Record = {}; + /** Keyed by `partLinkKey` — the (entry_id, part_id) pair — because two + * objects can carry the same part id, so part_id alone would merge pools. */ + @state() private _usedParts: Record = {}; public open(): void { if (this._open) return; @@ -55,8 +67,9 @@ export class MaintenanceCompleteDialog extends LitElement { this._readingValue = ""; this._restockQty = this.restockDefault !== null ? String(this.restockDefault) : ""; // #99: prefill "parts used" with the task's fixed links — the user can - // untick or adjust before completing. - this._usedParts = Object.fromEntries(this.consumesParts.map((l) => [l.part_id, l.quantity])); + // untick or adjust before completing. The whole link is kept, entry_id + // included, so a shared pool survives the edit (#111). + this._usedParts = Object.fromEntries(this.consumesParts.map((l) => [partLinkKey(l), { ...l }])); } private _toggleCheck(idx: number): void { @@ -149,10 +162,16 @@ export class MaintenanceCompleteDialog extends LitElement { } // #99: with a parts section shown, send the explicit selection — it // replaces the automatic consumes_parts deduction (empty = none used). + // entry_id travels only when the pool is somebody else's (#111), so an + // own-part payload is byte-identical to what shipped before. if (this.parts.length > 0) { - data.used_parts = Object.entries(this._usedParts) - .filter(([, qty]) => Number.isFinite(qty) && qty > 0) - .map(([part_id, quantity]) => ({ part_id, quantity })); + data.used_parts = Object.values(this._usedParts) + .filter((l) => Number.isFinite(l.quantity) && l.quantity > 0) + .map((l) => + l.entry_id + ? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id } + : { part_id: l.part_id, quantity: l.quantity }, + ); } await this.hass.connection.sendMessagePromise(data); this._open = false; @@ -164,6 +183,28 @@ export class MaintenanceCompleteDialog extends LitElement { } } + /** Required details the user has not supplied yet (drives Save + markers). */ + private get _missingRequired(): string[] { + const filled: Record = { + notes: this._notes.trim() !== "", + cost: this._cost.trim() !== "", + duration: this._duration.trim() !== "", + photo: this._photoDocId !== "", + // "Who did it" is filled in server-side from the authenticated + // connection (websocket/tasks_actions.py), so the dialog satisfies it + // as long as we ARE a logged-in user. Claiming it is always satisfied + // was how a task requiring "user" ended up unclosable: Save stayed + // enabled and the backend rejected the completion every time. + user: !!this.hass?.user, + }; + return this.requiredFields.filter((f) => !filled[f]); + } + + /** Marker appended to a required field's label. */ + private _req(field: string) { + return this.requiredFields.includes(field) ? html`` : nothing; + } + private _close(): void { this._open = false; } @@ -200,25 +241,36 @@ export class MaintenanceCompleteDialog extends LitElement { ? html`
${t("complete_parts_used", L)} ${this.parts.map((pt) => { - const qty = this._usedParts[pt.id]; - const checked = qty !== undefined; + const key = partLinkKey({ part_id: pt.id, entry_id: pt.entry_id }); + const link = this._usedParts[key]; + const checked = link !== undefined; + const base: TaskPartLink = pt.entry_id + ? { part_id: pt.id, quantity: 1, entry_id: pt.entry_id } + : { part_id: pt.id, quantity: 1 }; return html`
${checked ? html` { const v = parseFloat((e.target as HTMLInputElement).value); - this._usedParts = { ...this._usedParts, [pt.id]: Number.isFinite(v) && v >= 0.01 ? v : 1 }; + this._usedParts = { + ...this._usedParts, + [key]: { ...base, quantity: Number.isFinite(v) && v >= 0.01 ? v : 1 }, + }; }} />` : nothing}
`; @@ -245,25 +297,25 @@ export class MaintenanceCompleteDialog extends LitElement { only sees the title + Cancel/Complete buttons — the original bug report. Native inputs always render. -->
- ${t("completion_photo_optional", L)} + ${t("completion_photo_optional", L)}${this._req("photo")} ${this._photoPreview ? html`
@@ -306,7 +358,10 @@ export class MaintenanceCompleteDialog extends LitElement { 0} + title=${this._missingRequired.length + ? this._missingRequired.map((f) => t("err_required", L).replace("{field}", t(REQUIRED_COMPLETION_LABELS[f] ?? f, L))).join(" · ") + : ""} > ${this._loading ? t("completing", L) : t("complete", L)} @@ -316,6 +371,11 @@ export class MaintenanceCompleteDialog extends LitElement { } static styles = [nativeFieldStyles, css` + .req-mark { + color: var(--error-color, #f44336); + margin-left: 2px; + font-weight: 600; + } .dialog-title { font-size: 18px; font-weight: 500; @@ -348,6 +408,9 @@ export class MaintenanceCompleteDialog extends LitElement { font-size: 13px; cursor: pointer; } .used-part-check input { cursor: pointer; } + /* #111: whose stock this row draws on. Muted but never omitted — an + unlabelled foreign pool is indistinguishable from an own part. */ + .used-part-owner { color: var(--secondary-text-color); } .used-part-qty { width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px; border: 1px solid var(--divider-color); diff --git a/custom_components/maintenance_supporter/frontend-src/components/qr-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/qr-dialog.ts index 1db7fc7..510e7cb 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/qr-dialog.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/qr-dialog.ts @@ -155,9 +155,15 @@ export class MaintenanceQrDialog extends LitElement { const completeLabel = escapeHtml(t("qr_action_complete", L)); w.document.write(` + ${safeTitle} -

${c}

+

${d}

${u?`
${u}
`:""}
- QR Info -
${f}
+ QR Info +
${v}
${_?`
- QR Complete -
${m}
+ QR Complete +
${f}
`:""}
-
${le(this._viewResult.url)}
+
${de(this._viewResult.url)}