diff --git a/.storage/lovelace_resources b/.storage/lovelace_resources index fbadc7e1..f7d90e56 100644 --- a/.storage/lovelace_resources +++ b/.storage/lovelace_resources @@ -51,7 +51,7 @@ }, { "id": "6ad5d0d7a4c9435a85476e49d22d4df4", - "url": "/hacsfiles/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js?hacstag=714354847330", + "url": "/hacsfiles/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js?hacstag=714354847340", "type": "module" }, { diff --git a/custom_components/ha_mcp_tools/__init__.py b/custom_components/ha_mcp_tools/__init__.py index 80797dd5..424015aa 100644 --- a/custom_components/ha_mcp_tools/__init__.py +++ b/custom_components/ha_mcp_tools/__init__.py @@ -271,8 +271,15 @@ ALLOWED_READ_FILES = [ "scenes.yaml", "secrets.yaml", "home-assistant.log", + # faulthandler's crash dump (HA Core's ``__main__`` enables it on this file). + # Written only on a native fatal signal, so it is the one place that + # traceback exists: journald / error_log never see it (issue #2373). + "home-assistant.log.fault", ] +# Root-level log files that the read service tails by default. +TAILED_LOG_FILES = frozenset({"home-assistant.log", "home-assistant.log.fault"}) + # Default tail lines for log files DEFAULT_LOG_TAIL_LINES = 1000 @@ -1345,6 +1352,53 @@ def _read_file_sync(target_file: Path) -> dict[str, Any]: return {"content": content, "size": stat.st_size, "mtime": stat.st_mtime} +def _read_log_tail_sync( + target_file: Path, tail_lines: int, *, chunk_size: int = 1 << 16 +) -> dict[str, Any]: + """Tail a log for one executor offload without materialising the whole file. + + HA Core appends to ``home-assistant.log.fault`` for the life of the + install, so reading the file whole just to keep its last ``tail_lines`` + lines scales with the crash history rather than the request. Line count + streams the file in chunks; the tail seeks back from the end until it holds + ``tail_lines`` newlines. The result carries the same ``content`` / + ``total_lines`` / ``truncated`` the whole-file split produced (``split("\n")`` + semantics, so a trailing newline yields one empty last line), plus + ``size``/``mtime`` like ``_read_file_sync``. + """ + if not target_file.exists(): + return {"_error": "not_found"} + if not target_file.is_file(): + return {"_error": "not_a_file"} + stat = target_file.stat() + newlines = 0 + with target_file.open("rb") as fh: + for chunk in iter(lambda: fh.read(chunk_size), b""): + newlines += chunk.count(b"\n") + total_lines = newlines + 1 + pos = fh.tell() + buf = b"" + while pos > 0 and buf.count(b"\n") < tail_lines: + step = min(chunk_size, pos) + pos -= step + fh.seek(pos) + buf = fh.read(step) + buf + parts = buf.split(b"\n") + truncated = total_lines > tail_lines + if truncated: + # The leading part may start mid-character at a chunk boundary; it is + # never part of the tail, so decode only what is returned. + parts = parts[-tail_lines:] + content = b"\n".join(parts).decode("utf-8") + return { + "content": content, + "size": stat.st_size, + "mtime": stat.st_mtime, + "total_lines": total_lines, + "truncated": truncated, + } + + def _write_file_sync( target_file: Path, content: str, @@ -2772,25 +2826,19 @@ async def _shape_read_file_response( if normalized == "secrets.yaml": content = await hass.async_add_executor_job(_mask_secrets_content, content) - # Apply tail for log files - if normalized == "home-assistant.log": - lines = content.split("\n") + # Log files arrive already tailed by ``_read_log_tail_sync``. + if normalized in TAILED_LOG_FILES: limit = tail_lines if tail_lines else DEFAULT_LOG_TAIL_LINES - if len(lines) > limit: - content = "\n".join(lines[-limit:]) - truncated = True - else: - truncated = False - + total_lines = result["total_lines"] return { "success": True, "path": rel_path, "content": content, "size": stat_size, "modified": modified_dt.isoformat(), - "lines_returned": min(len(lines), limit), - "total_lines": len(lines), - "truncated": truncated, + "lines_returned": min(total_lines, limit), + "total_lines": total_lines, + "truncated": result["truncated"], } # Apply tail for other files if requested @@ -2853,13 +2901,22 @@ def _build_read_file_handler( ) return { "success": False, - "error": f"Path not allowed. Allowed patterns: {', '.join(allowed_patterns)}", + "error": f"Path not allowed. Allowed paths: {', '.join(allowed_patterns)}", } target_file = config_dir / rel_path try: - result = await hass.async_add_executor_job(_read_file_sync, target_file) + if os.path.normpath(rel_path) in TAILED_LOG_FILES: # noqa: ASYNC240 + # Bounded read: the fault log is append-only for the life of + # the install, so never materialise the whole file for a tail. + result = await hass.async_add_executor_job( + _read_log_tail_sync, + target_file, + tail_lines if tail_lines else DEFAULT_LOG_TAIL_LINES, + ) + else: + result = await hass.async_add_executor_job(_read_file_sync, target_file) except PermissionError: _LOGGER.error("Permission denied reading: %s", rel_path) return { diff --git a/custom_components/ha_mcp_tools/const.py b/custom_components/ha_mcp_tools/const.py index 53c5da17..4122462b 100644 --- a/custom_components/ha_mcp_tools/const.py +++ b/custom_components/ha_mcp_tools/const.py @@ -25,7 +25,7 @@ DOMAIN = "ha_mcp_tools" # in CI. The # capability negotiation — not this version — gates each WS command (see # ``websocket_api.CAPABILITIES``). -COMPONENT_VERSION = "2.1.3" +COMPONENT_VERSION = "2.2.0" # Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value # means "tools" so the pre-existing services entry keeps working across the @@ -53,9 +53,10 @@ MIN_EMBEDDED_HOME_ASSISTANT_VERSION = "2026.8.0" # allowlist, not an absolute guarantee: an admin who adds "blueprints" as a # custom extra directory (issue #1567, see _current_extra_dirs) grants it # read+write, since extra_dirs are honored on the write path too. Blueprint -# writes should instead go through ha_import_blueprint (which invokes the -# blueprint/save WS command internally). Prefer ha_get_blueprint for the parsed -# body; raw read is the escape hatch for the exact on-disk text. +# writes should instead go through ha_manage_blueprints(action="import") (which +# invokes the blueprint/save WS command internally). Prefer +# ha_manage_blueprints(action="get") for the parsed body; raw read is the escape +# hatch for the exact on-disk text. ALLOWED_READ_DIRS = ["www", "themes", "custom_templates", "dashboards", "blueprints"] ALLOWED_WRITE_DIRS = ["www", "themes", "custom_templates", "dashboards"] diff --git a/custom_components/ha_mcp_tools/embedded_server.py b/custom_components/ha_mcp_tools/embedded_server.py index 77ffece9..93139809 100644 --- a/custom_components/ha_mcp_tools/embedded_server.py +++ b/custom_components/ha_mcp_tools/embedded_server.py @@ -1520,6 +1520,49 @@ class EmbeddedServerManager: # -- worker thread ----------------------------------------------------- + def _register_embedded_connection( + self, hamcp_config: Any, access_token: str + ) -> None: + """Hand the loopback URL, admin token and config dir over in memory. + + Optional keywords are dropped one at a time, newest first, when the + installed server rejects them with ``TypeError``: an older release + predates ``config_dir`` (issue #2329) or ``verify_ssl`` (issue #1890). + Only the rejected keyword is dropped — falling straight back to the + two-argument call would re-open #1890's certificate failure on an + SSL-enabled instance just because the server is missing an unrelated, + newer parameter. + """ + kwargs: dict[str, Any] = {"config_dir": self._hass_config_dir} + if self._loopback_verify_ssl is not None: + kwargs["verify_ssl"] = self._loopback_verify_ssl + for optional in ("config_dir", "verify_ssl", None): + try: + hamcp_config.set_embedded_connection( + self._server_url, access_token, **kwargs + ) + return + except TypeError: + if optional is None: + raise + if optional not in kwargs: + continue + del kwargs[optional] + if optional == "config_dir": + # Only costs the direct blueprint file read; the server + # falls back to reading through this component. + _LOGGER.debug( + "Installed ha-mcp server does not accept config_dir for " + "the embedded connection; blueprint reads route through " + "the component until the server package updates" + ) + else: + _LOGGER.warning( + "Installed ha-mcp server does not accept verify_ssl for " + "the embedded connection; loopback TLS verification stays " + "enabled until the server package updates" + ) + def _note_startup_phase(self, phase: str) -> None: """Publish the worker's startup phase (a plain attribute write). @@ -1629,27 +1672,7 @@ class EmbeddedServerManager: "entry may serve stale override values until HA restarts" ) - if self._loopback_verify_ssl is None: - _hamcp_config.set_embedded_connection(self._server_url, access_token) - else: - try: - _hamcp_config.set_embedded_connection( - self._server_url, - access_token, - verify_ssl=self._loopback_verify_ssl, - ) - except TypeError: - # Server predates the verify_ssl parameter (< the release - # carrying issue #1890's fix). Register url+token the old way; - # on an SSL-enabled instance the wss loopback will fail - # certificate verification until the server package updates — - # no worse than the plaintext failure it replaces. - _LOGGER.warning( - "Installed ha-mcp server does not accept verify_ssl for " - "the embedded connection; loopback TLS verification stays " - "enabled until the server package updates" - ) - _hamcp_config.set_embedded_connection(self._server_url, access_token) + self._register_embedded_connection(_hamcp_config, access_token) # Imported here, in the worker thread, after the connection is registered. from ha_mcp.server import HomeAssistantSmartMCPServer diff --git a/custom_components/ha_mcp_tools/llm_api.py b/custom_components/ha_mcp_tools/llm_api.py index 65299a12..c49070de 100644 --- a/custom_components/ha_mcp_tools/llm_api.py +++ b/custom_components/ha_mcp_tools/llm_api.py @@ -33,9 +33,9 @@ read-only middleware exactly like any MCP client's call. The server runs on its own worker thread behind a loopback HTTP listener, and ``ha_mcp`` must never be imported in the HA main process (see :mod:`embedded_server`), so this module talks real MCP to the server over -loopback streamable HTTP. The ``mcp`` client SDK arrives with the -runtime-installed ha-mcp package (a fastmcp dependency), so every SDK import -here is lazy and the first one runs on the executor. +loopback streamable HTTP with Home Assistant's shared ``mcp`` SDK (the manifest +requires ``mcp>=1.24.0``, so 1.x or 2.x), never ha-mcp's vendored copy. Every SDK import here +is lazy and the first one runs on the executor. The tool list is fetched fresh on every ``async_get_api_instance`` call (once per conversation turn): exposure toggles and runtime-registered custom tools @@ -51,6 +51,7 @@ import copy import importlib import logging import math +import sys from collections import Counter from collections.abc import AsyncIterator, Callable, Iterable from contextlib import AsyncExitStack, asynccontextmanager @@ -389,28 +390,37 @@ def _transport_error_leaves() -> tuple[type[BaseException], ...]: """Return the non-group exception classes a loopback exchange can raise. OSError covers a refused/dropped loopback connect; TimeoutError comes - from our asyncio.timeout budget. httpx errors and protocol-level McpError - can also escape a session call UNWRAPPED (HA core's mcp integration - catches both the same way), but neither class is importable at module - level — both arrive with the runtime-installed server package — hence a - function instead of a module constant. + from our asyncio.timeout budget. HTTP-client errors and the protocol-level + MCP error can also escape a session call UNWRAPPED (HA core's mcp + integration catches both the same way); SDK 1.x raises ``httpx`` / + ``McpError``, 2.x raises ``httpx2`` / ``MCPError``. Read from ``sys.modules``: + the SDK already imported whichever it raised from, and this runs on the + event loop, where an import is a blocking call. """ - errors: tuple[type[BaseException], ...] = (TimeoutError, OSError) - try: - import httpx - from mcp import McpError - except ImportError: # pragma: no cover - SDK-less builds never open a session - return errors - return (*errors, httpx.HTTPError, McpError) + errors: list[type[BaseException]] = [TimeoutError, OSError] + for module_name, attribute in ( + ("httpx", "HTTPError"), + ("httpx2", "HTTPError"), + ("mcp", "McpError"), + ("mcp", "MCPError"), + ): + module = sys.modules.get(module_name) + if module is None: + continue + error_class = getattr(module, attribute, None) + if isinstance(error_class, type) and error_class not in errors: + errors.append(error_class) + return tuple(errors) def _transport_errors() -> tuple[type[BaseException], ...]: """Return the ``except`` target for one loopback MCP exchange. - Evaluated at exception time (an ``except`` expression is), so the lazy - imports in :func:`_transport_error_leaves` have already succeeded by - then. Includes ExceptionGroup because the SDK's anyio task groups wrap - in-session failures — but a caught group must still pass + Evaluated at exception time (an ``except`` expression is), so whichever + HTTP/MCP module raised is already in ``sys.modules`` for + :func:`_transport_error_leaves` to find. Includes ExceptionGroup because + the SDK's anyio task groups wrap in-session failures — but a caught group + must still pass :func:`_is_transport_failure` before being mapped to a friendly error, or a genuine bug that happened inside the task group would be relabeled as a transport failure (review finding). @@ -547,34 +557,40 @@ async def _mcp_session( """ from mcp.client.session import ClientSession + sdk_transport = importlib.import_module("mcp.client.streamable_http") + async with AsyncExitStack() as stack: - try: - from mcp.client.streamable_http import streamable_http_client - except ImportError: - # Pre-rename SDK (an older ha-mcp resolved by a pip-spec override - # pins an older fastmcp/mcp): same call shape, deprecated name, + streamable_http_client = getattr(sdk_transport, "streamable_http_client", None) + if streamable_http_client is None: + # Pre-1.24 SDK (a third-party pin forcing the shared mcp below the + # manifest's floor): same call shape, deprecated name, # and no http_client kwarg — but it does accept a factory for the # client it builds internally, so _loopback_httpx_client_factory # keeps this fallback on the same verify=False/trust_env=False # posture as the canonical path below. - from mcp.client.streamable_http import streamablehttp_client - - transport = streamablehttp_client( + transport = sdk_transport.streamablehttp_client( url=url, httpx_client_factory=_loopback_httpx_client_factory ) else: - import httpx - + # SDK 1.x takes an httpx client, 2.x an httpx2 one: use the library + # the transport module itself imported. + http_lib = getattr(sdk_transport, "httpx2", None) or getattr( + sdk_transport, "httpx", None + ) + if http_lib is None: + http_lib = importlib.import_module("httpx") http_client = await stack.enter_async_context( - httpx.AsyncClient( + http_lib.AsyncClient( verify=False, trust_env=False, - timeout=httpx.Timeout(_CALL_TOOL_TIMEOUT_SECONDS), + timeout=http_lib.Timeout(_CALL_TOOL_TIMEOUT_SECONDS), ) ) transport = streamable_http_client(url=url, http_client=http_client) - read_stream, write_stream, _ = await stack.enter_async_context(transport) + # 1.x yields (read, write, get_session_id); 2.x yields (read, write). + streams = await stack.enter_async_context(transport) + read_stream, write_stream = streams[0], streams[1] session = await stack.enter_async_context( ClientSession(read_stream, write_stream) ) @@ -673,8 +689,30 @@ async def _forward_tool_call( # Full CallToolResult (content blocks, structuredContent, isError) — # the same shape HA core's mcp integration hands to agents; ha-mcp # signals tool failure via isError + structured error JSON, which the - # agent reads and reacts to like any tool output. - return result.model_dump(exclude_unset=True, exclude_none=True) + # agent reads and reacts to like any tool output. Dumped by alias (2.x + # fields are snake_case) with ``meta`` kept at its 1.x key, so both SDK + # lines hand agents the same keys. + dumped = result.model_dump(exclude_unset=True, exclude_none=True, by_alias=True) + return cast("JsonObjectType", _meta_as_field_name(dumped)) + + +def _meta_as_field_name(dumped: dict[str, Any]) -> dict[str, Any]: + """Rename ``_meta`` to ``meta`` on the SDK models only, never in tool payloads.""" + models: list[Any] = [dumped] + for block in dumped.get("content", []): + models += [block, block.get("resource") if isinstance(block, dict) else None] + for model in models: + if isinstance(model, dict) and "_meta" in model: + model["meta"] = model.pop("_meta") + return dumped + + +def _tool_input_schema(tool: Any) -> Any: + """``input_schema`` on SDK 2.x, ``inputSchema`` on 1.x.""" + schema = getattr(tool, "input_schema", None) + if schema is None: + schema = getattr(tool, "inputSchema", None) + return schema def _search_score(query_words: list[str], name: str, description: str) -> int: @@ -868,7 +906,7 @@ class HaMcpLlmApi(llm.API): """Mirror every exposed tool directly (full-catalog mode).""" tools: list[llm.Tool] = [] for tool in exposed: - schema = _normalise_schema(tool.inputSchema, tool.name) + schema = _normalise_schema(_tool_input_schema(tool), tool.name) parameters = self._convert_parameters(tool, schema) if parameters is None: continue @@ -896,7 +934,7 @@ class HaMcpLlmApi(llm.API): # recorded no mutating call at all. A # converter that wrote back would corrupt the catalog entry, so # re-check this before pointing the component at a third one. - schema = _normalise_schema(tool.inputSchema, tool.name) + schema = _normalise_schema(_tool_input_schema(tool), tool.name) catalog.append( { "name": tool.name, diff --git a/custom_components/ha_mcp_tools/manifest.json b/custom_components/ha_mcp_tools/manifest.json index ce518c7f..69277c30 100644 --- a/custom_components/ha_mcp_tools/manifest.json +++ b/custom_components/ha_mcp_tools/manifest.json @@ -20,8 +20,9 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/homeassistant-ai/ha-mcp/issues", "requirements": [ + "mcp>=1.24.0", "ruamel.yaml>=0.18.0", "voluptuous-openapi>=0.4.1" ], - "version": "2.1.3" + "version": "2.2.0" } diff --git a/custom_components/ha_mcp_tools/mcp_webhook.py b/custom_components/ha_mcp_tools/mcp_webhook.py index 77dd85af..f69b7749 100644 --- a/custom_components/ha_mcp_tools/mcp_webhook.py +++ b/custom_components/ha_mcp_tools/mcp_webhook.py @@ -72,12 +72,24 @@ from .oauth_legacy import ( build_unbound_legacy_provider, clear_scoped_legacy_credentials, ) +from .readonly_webhook import ( + readonly_url, + register_readonly_webhook, + unregister_readonly_webhook, +) if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry _LOGGER = logging.getLogger(__name__) + +def _unregister_webhook(hass: HomeAssistant, webhook_id: str) -> None: + """Remove both endpoint variants, including on partial setup failure.""" + unregister_readonly_webhook(hass, webhook_id) + async_unregister(hass, webhook_id) + + # Human-readable webhook name shown in the HA webhook registry. _WEBHOOK_NAME = "HA-MCP in-process server" @@ -585,7 +597,11 @@ async def _check_webhook_auth( async def _async_handle_webhook( - hass: HomeAssistant, webhook_id: str, request: web.Request + hass: HomeAssistant, + webhook_id: str, + request: web.Request, + *, + read_only: bool = False, ) -> web.StreamResponse: """Forward an MCP request to the loopback server and stream the reply back.""" domain_data = hass.data.get(DOMAIN) @@ -597,7 +613,9 @@ async def _async_handle_webhook( if auth_response is not None: return auth_response - target_url: str = cfg["target_url"] + target_url: str = ( + readonly_url(cfg["target_url"]) if read_only else cfg["target_url"] + ) session: aiohttp.ClientSession = cfg["session"] body = await request.read() @@ -824,7 +842,7 @@ async def async_register_webhook( # crashed unload before (re)registering — or before storing a local-only # config (async_unregister is a no-op pop when nothing is registered). # Runs before the session opens so a raise here cannot leak it. - async_unregister(hass, webhook_id) + _unregister_webhook(hass, webhook_id) target_url = f"http://127.0.0.1:{port}{secret_path}" session = aiohttp.ClientSession(timeout=_CLIENT_TIMEOUT) cimd_session: aiohttp.ClientSession | None = None @@ -862,6 +880,7 @@ async def async_register_webhook( _async_handle_webhook, allowed_methods=["POST", "GET"], ) + register_readonly_webhook(hass, webhook_id, _async_handle_webhook) if auth_mode == WEBHOOK_AUTH_HA: _bind_ha_auth_surface(hass, cfg, webhook_id, dcr_signing_key) elif auth_mode == WEBHOOK_AUTH_LEGACY: @@ -877,7 +896,7 @@ async def async_register_webhook( # auth-setup path. suppress: the ORIGINAL error must be what # propagates (review finding) - a raising cleanup would mask it. with suppress(Exception): - async_unregister(hass, webhook_id) + _unregister_webhook(hass, webhook_id) with suppress(Exception): await session.close() if cimd_session is not None: @@ -917,7 +936,7 @@ async def async_unregister_webhook(hass: HomeAssistant) -> None: return webhook_id = cfg.get("webhook_id") if webhook_id: - async_unregister(hass, webhook_id) + _unregister_webhook(hass, webhook_id) session = cfg.get("session") if session is not None: await session.close() diff --git a/custom_components/ha_mcp_tools/oauth_autoapprove.py b/custom_components/ha_mcp_tools/oauth_autoapprove.py index c229838a..34df7034 100644 --- a/custom_components/ha_mcp_tools/oauth_autoapprove.py +++ b/custom_components/ha_mcp_tools/oauth_autoapprove.py @@ -60,6 +60,7 @@ from .oauth_legacy import ( _PKCE_CHALLENGE_RE, _TOKEN_RESPONSE_HEADERS, ACCESS_TOKEN_TTL, + PKCE_S256_CHALLENGE_LEN, PKCECodeStore, _is_valid_redirect_uri, _issuer_for, @@ -165,20 +166,6 @@ def _webhook_cfg(hass: HomeAssistant) -> dict[str, Any] | None: return cfg if isinstance(cfg, dict) else None -def _active_autoapprove_provider(hass: HomeAssistant) -> AutoApproveProvider | None: - """The live none-mode auto-approve provider, or None when it is not live. - - Read live from ``hass.data`` (not captured at view construction) so the - bound views serve only while none-autoapprove is the active mode and 404 - otherwise — mirrors ``mcp_webhook._active_webhook_id``'s per-request gating. - """ - cfg = _webhook_cfg(hass) - if cfg is None: - return None - provider = cfg.get(CFG_AUTOAPPROVE_PROVIDER) - return provider if isinstance(provider, AutoApproveProvider) else None - - def _validate_autoapprove_authorize(params: Any) -> web.Response | None: """Validate the none-mode /authorize query; a 400 Response, or None if OK. @@ -197,7 +184,9 @@ def _validate_autoapprove_authorize(params: Any) -> web.Response | None: return _json_error("invalid_request", 400, "code_challenge_method must be S256") if not _PKCE_CHALLENGE_RE.fullmatch(params.get("code_challenge", "")): return _json_error( - "invalid_request", 400, "invalid code_challenge (43-char base64url)" + "invalid_request", + 400, + f"invalid code_challenge ({PKCE_S256_CHALLENGE_LEN}-char base64url)", ) if not _is_valid_redirect_uri(params.get("redirect_uri", "")): return _json_error("invalid_request", 400, "invalid redirect_uri") diff --git a/custom_components/ha_mcp_tools/oauth_legacy.py b/custom_components/ha_mcp_tools/oauth_legacy.py index 11f61210..64cbdc4c 100644 --- a/custom_components/ha_mcp_tools/oauth_legacy.py +++ b/custom_components/ha_mcp_tools/oauth_legacy.py @@ -89,7 +89,7 @@ PKCE_VERIFIER_MAX = 128 # SHA-256 → 32 bytes → 43 base64url chars (no padding). PKCE_S256_CHALLENGE_LEN = 43 _PKCE_VERIFIER_RE = re.compile(r"[A-Za-z0-9._~-]+") -_PKCE_CHALLENGE_RE = re.compile(r"[A-Za-z0-9_-]{43}") +_PKCE_CHALLENGE_RE = re.compile(rf"[A-Za-z0-9_-]{{{PKCE_S256_CHALLENGE_LEN}}}") # Pending-code dict cap. An attacker spamming /authorize with valid params # could grow the dict between the prune passes that run on each issuance. @@ -676,7 +676,8 @@ class LegacyOAuthProvider: return _text_error(400, "invalid code_challenge_method (S256 required)") if not _PKCE_CHALLENGE_RE.fullmatch(code_challenge): return _text_error( - 400, "invalid code_challenge (must be 43-char base64url)" + 400, + f"invalid code_challenge (must be {PKCE_S256_CHALLENGE_LEN}-char base64url)", ) if client_id != self.client_id: return _text_error(400, "invalid client_id", restart_hint=True) diff --git a/custom_components/ha_mcp_tools/services.yaml b/custom_components/ha_mcp_tools/services.yaml index c5f2dc25..8ff6a27f 100644 --- a/custom_components/ha_mcp_tools/services.yaml +++ b/custom_components/ha_mcp_tools/services.yaml @@ -83,7 +83,8 @@ read_file: description: >- Relative path from config directory. Allowed paths include configuration.yaml, automations.yaml, scripts.yaml, scenes.yaml, - secrets.yaml (values masked), home-assistant.log, www/**, themes/**, + secrets.yaml (values masked), home-assistant.log, + home-assistant.log.fault, www/**, themes/**, custom_templates/**, dashboards/**, blueprints/**, packages/*.yaml, custom_components/**/*.py required: true diff --git a/custom_components/ha_mcp_tools/strings.json b/custom_components/ha_mcp_tools/strings.json index 37e89419..937bdf9a 100644 --- a/custom_components/ha_mcp_tools/strings.json +++ b/custom_components/ha_mcp_tools/strings.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Extra YAML write keys" }, "data_description": { - "allowed_dirs": "Paths relative to the config directory, including exact filenames such as sensor.yaml, or paths under /share, /media, /ssl, and /backup. Each entry is granted read and write for that path and anything below it; enter every path as a separate list item. Traversal and other disallowed paths are dropped.", + "allowed_dirs": "Paths relative to the config directory, including exact filenames such as sensor.yaml, or paths under /share, /media, /ssl, and /backup. Each entry is granted read and write for that path and anything below it; enter every path as a separate list item. Entries are matched literally; wildcards such as * are not supported. Traversal and other disallowed paths are dropped.", "extra_yaml_keys": "Top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Trust-boundary keys are dropped." } }, diff --git a/custom_components/ha_mcp_tools/translations/cs.json b/custom_components/ha_mcp_tools/translations/cs.json index 87ff20c3..97136eae 100644 --- a/custom_components/ha_mcp_tools/translations/cs.json +++ b/custom_components/ha_mcp_tools/translations/cs.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Další klíče pro zápis YAML" }, "data_description": { - "allowed_dirs": "Cesty relativní k adresáři konfigurace, včetně přesných názvů souborů, jako je sensor.yaml, nebo cesty pod /share, /media, /ssl a /backup. Každá položka má uděleno oprávnění pro čtení a zápis pro danou cestu a vše pod ní; každou cestu zadejte jako samostatnou položku seznamu. Adresářový posun (traversal) a jiné nepovolené cesty budou ignorovány.", + "allowed_dirs": "Cesty relativní ke složce konfigurační složky, včetně přesných názvů souborů jako sensor.yaml, nebo cesty pod /share, /media, /ssl a /backup. Každá položka má uděleno právo čtení i zápisu pro danou cestu a vše pod ní; zadejte každou cestu jako samostatnou položku seznamu. Položky se porovnávají doslovně; zástupné znaky jako * nejsou podporovány. Průchod adresářovou strukturou a jiné nepovolené cesty jsou zahazovány.", "extra_yaml_keys": "Klíče nejvyšší úrovně, které ha_config_set_yaml může zapisovat navíc k vestavěným, pro integrace s prioritou YAML v této instalaci (např. alert2). Klíče hranice důvěry jsou zahozeny." } }, diff --git a/custom_components/ha_mcp_tools/translations/de.json b/custom_components/ha_mcp_tools/translations/de.json index 3faa7816..00d32e10 100644 --- a/custom_components/ha_mcp_tools/translations/de.json +++ b/custom_components/ha_mcp_tools/translations/de.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Zusätzliche YAML-Schreib-Keys" }, "data_description": { - "allowed_dirs": "Pfade relativ zum Konfigurationsverzeichnis, einschließlich genauer Dateinamen wie sensor.yaml, oder Pfade unter /share, /media, /ssl und /backup. Jedem Eintrag wird Lese- und Schreibzugriff für diesen Pfad und alles darunter gewährt; gib jeden Pfad als separaten Listeneintrag an. Verzeichnistraversierung und andere nicht erlaubte Pfade werden verworfen.", + "allowed_dirs": "Pfade relativ zum Konfigurationsverzeichnis, einschließlich exakter Dateinamen wie sensor.yaml, oder Pfade unter /share, /media, /ssl und /backup. Jedem Eintrag wird Lese- und Schreibzugriff für diesen Pfad und alles darunter gewährt; gib jeden Pfad als separates Listenelement ein. Einträge werden wörtlich abgeglichen; Platzhalter wie * werden nicht unterstützt. Pfadtraversierung und andere unzulässige Pfade werden verworfen.", "extra_yaml_keys": "Top-Level-Keys, die ha_config_set_yaml zusätzlich zu den eingebauten schreiben darf, für YAML-first-Integrationen auf dieser Installation (z. B. alert2). Vertrauensgrenzen-Keys werden verworfen." } }, diff --git a/custom_components/ha_mcp_tools/translations/en.json b/custom_components/ha_mcp_tools/translations/en.json index 37e89419..937bdf9a 100644 --- a/custom_components/ha_mcp_tools/translations/en.json +++ b/custom_components/ha_mcp_tools/translations/en.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Extra YAML write keys" }, "data_description": { - "allowed_dirs": "Paths relative to the config directory, including exact filenames such as sensor.yaml, or paths under /share, /media, /ssl, and /backup. Each entry is granted read and write for that path and anything below it; enter every path as a separate list item. Traversal and other disallowed paths are dropped.", + "allowed_dirs": "Paths relative to the config directory, including exact filenames such as sensor.yaml, or paths under /share, /media, /ssl, and /backup. Each entry is granted read and write for that path and anything below it; enter every path as a separate list item. Entries are matched literally; wildcards such as * are not supported. Traversal and other disallowed paths are dropped.", "extra_yaml_keys": "Top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Trust-boundary keys are dropped." } }, diff --git a/custom_components/ha_mcp_tools/translations/eo.json b/custom_components/ha_mcp_tools/translations/eo.json index 291392e2..e1abe9ee 100644 --- a/custom_components/ha_mcp_tools/translations/eo.json +++ b/custom_components/ha_mcp_tools/translations/eo.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Ekstraj YAML-skribaj ŝlosiloj" }, "data_description": { - "allowed_dirs": "Vojoj relative al la agorda dosierujo, inkluzive de ekzakta dosiernomo kiel sensor.yaml, aŭ vojoj sub /share, /media, /ssl, kaj /backup. Ĉiu eniro ricevas leg- kaj skrib-permeson por tiu vojo kaj ĉio sub ĝi; enigu ĉiun vojon kiel apartan listan eron. Trairado kaj aliaj nepermesitaj vojoj estas forigitaj.", + "allowed_dirs": "Poj do la agorda dosierujo, inkluzive de ekzakta dosiernomo kiel sensor.yaml, aŭ vojoj sub /share, /media, /ssl, kaj /backup. Ĉiu eniro ricevas leg- kaj skrib-permeson por tiu vojo kaj ĉio sub ĝi; enigu ĉiun vojon kiel apartan listan eron. Eniroj estas kongruigataj laŭlitere; sovaĝaj signoj kiel * ne estas subtenataj. Trapasado kaj aliaj nepermesitaj vojoj estas forĵetitaj.", "extra_yaml_keys": "Ĉefnivelaj ŝlosiloj, kiujn ha_config_set_yaml povas skribi krom la enkonstruitaj, por YAML-unuaj integriĝoj en ĉi tiu instalaĵo (ekz. alert2). Fid-limaj ŝlosiloj estas forĵetitaj." } }, diff --git a/custom_components/ha_mcp_tools/translations/es.json b/custom_components/ha_mcp_tools/translations/es.json index 4671026c..fe6067df 100644 --- a/custom_components/ha_mcp_tools/translations/es.json +++ b/custom_components/ha_mcp_tools/translations/es.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Claves YAML de escritura adicionales" }, "data_description": { - "allowed_dirs": "Rutas relativas al directorio de configuración, incluidos nombres de archivo exactos como sensor.yaml, o rutas bajo /share, /media, /ssl y /backup. A cada entrada se le concede lectura y escritura para esa ruta y todo lo que esté debajo de ella; introduce cada ruta como un elemento de lista separado. Se descartarán los recorridos de directorios y otras rutas no permitidas.", + "allowed_dirs": "Rutas relativas al directorio de configuración, incluidos nombres de archivo exactos como sensor.yaml, o rutas bajo /share, /media, /ssl y /backup. A cada entrada se le concede lectura y escritura para esa ruta y todo lo que esté debajo de ella; introduce cada entrada como un elemento de lista separado. Las entradas se comparan literalmente; no se admiten comodines como *. Se omiten las rutas con recorrido de directorios y otras no permitidas.", "extra_yaml_keys": "Claves de primer nivel que ha_config_set_yaml puede escribir además de las integradas, para integraciones basadas en YAML de esta instalación (p. ej. alert2). Las claves de la frontera de confianza se descartan." } }, diff --git a/custom_components/ha_mcp_tools/translations/fr.json b/custom_components/ha_mcp_tools/translations/fr.json index 3ac55c39..e3ab5322 100644 --- a/custom_components/ha_mcp_tools/translations/fr.json +++ b/custom_components/ha_mcp_tools/translations/fr.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Clés d'écriture YAML supplémentaires" }, "data_description": { - "allowed_dirs": "Chemins relatifs au répertoire de configuration, incluant les noms de fichiers exacts tels que sensor.yaml, ou les chemins sous /share, /media, /ssl et /backup. Chaque entrée bénéficie d'un accès en lecture et en écriture pour ce chemin et tout ce qu'il contient ; saisissez chaque chemin comme un élément de liste séparé. Les parcours de répertoires (traversal) et autres chemins non autorisés sont ignorés.", + "allowed_dirs": "Chemins relatifs au répertoire de configuration, y compris les noms de fichiers exacts tels que sensor.yaml, ou les chemins sous /share, /media, /ssl et /backup. Chaque entrée se voit accorder un accès en lecture et en écriture pour ce chemin et tout ce qui se trouve en dessous ; entrez chaque chemin comme un élément de liste séparé. Les entrées sont comparées littéralement ; les caractères génériques tels que * ne sont pas pris en charge. Les traversées de répertoires et autres chemins non autorisés sont ignorés.", "extra_yaml_keys": "Clés de premier niveau que ha_config_set_yaml peut écrire en plus de celles intégrées, pour les intégrations YAML-first de cette installation (ex. alert2). Les clés de limite de confiance sont ignorées." } }, diff --git a/custom_components/ha_mcp_tools/translations/it.json b/custom_components/ha_mcp_tools/translations/it.json index 775e347c..c3f55ed5 100644 --- a/custom_components/ha_mcp_tools/translations/it.json +++ b/custom_components/ha_mcp_tools/translations/it.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Chiavi YAML scrivibili aggiuntive" }, "data_description": { - "allowed_dirs": "Percorsi relativi alla directory di configurazione, inclusi nomi di file esatti come sensor.yaml, o percorsi sotto /share, /media, /ssl e /backup. A ciascuna voce viene concesso l'accesso in lettura e scrittura per quel percorso e per tutto ciò che si trova al di sotto; inserisci ogni percorso come elemento di elenco separato. I percorsi di attraversamento e altri non consentiti vengono scartati.", + "allowed_dirs": "Percorsi relativi alla directory di configurazione, inclusi nomi file esatti come sensor.yaml, o percorsi sotto /share, /media, /ssl e /backup. A ogni voce vengono concessi i permessi di lettura e scrittura per quel percorso e per tutto ciò che si trova al di sotto di esso; inserisci ogni percorso come elemento di elenco separato. Le voci vengono confrontate letteralmente; i caratteri jolly come * non sono supportati. I percorsi di attraversamento e altri percorsi non consentiti vengono scartati.", "extra_yaml_keys": "Chiavi di primo livello che ha_config_set_yaml può scrivere oltre a quelle integrate, per le integrazioni basate su YAML di questa installazione (per esempio alert2). Le chiavi del confine di fiducia vengono scartate." } }, diff --git a/custom_components/ha_mcp_tools/translations/ko.json b/custom_components/ha_mcp_tools/translations/ko.json index ea65fb15..fa1b717a 100644 --- a/custom_components/ha_mcp_tools/translations/ko.json +++ b/custom_components/ha_mcp_tools/translations/ko.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "추가 YAML 쓰기 허용 키" }, "data_description": { - "allowed_dirs": "sensor.yaml 같은 정확한 파일 이름을 포함하여 config 디렉토리 상대 경로, 또는 /share, /media, /ssl, /backup 아래의 경로입니다. 각 항목에는 해당 경로 및 그 하위 경로에 대한 읽기 및 쓰기 권한이 부여되며, 각 경로는 별도의 목록 항목으로 입력하세요. 상위 디렉토리 순회 및 허용되지 않는 경로는 제외됩니다.", + "allowed_dirs": "config 디렉터리를 기준으로 한 상대 경로이며, sensor.yaml 같은 정확한 파일 이름이나 /share, /media, /ssl, /backup 아래의 경로를 포함합니다. 각 항목은 해당 경로와 그 하위 항목에 대한 읽기 및 쓰기 권한이 부여되며, 모든 경로는 별도의 목록 항목으로 입력해야 합니다. 항목은 정확히 일치하는 문자열로 처리되며, * 같은 와일드카드는 지원되지 않습니다. 상위 디렉터리 접근(Traversal) 및 허용되지 않는 경로는 무시됩니다.", "extra_yaml_keys": "이 설치본에서 YAML 우선으로 동작하는 통합 구성요소(예: alert2)를 위해, ha_config_set_yaml이 기본 허용 키 외에 추가로 쓸 수 있는 최상위 키입니다. 신뢰 경계 키는 무시됩니다." } }, diff --git a/custom_components/ha_mcp_tools/translations/nl.json b/custom_components/ha_mcp_tools/translations/nl.json index 63250528..d523229c 100644 --- a/custom_components/ha_mcp_tools/translations/nl.json +++ b/custom_components/ha_mcp_tools/translations/nl.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Extra YAML-schrijfsleutels" }, "data_description": { - "allowed_dirs": "Paden relatief ten opzichte van deconfiguratiemap, inclusief exacte bestandsnamen zoals sensor.yaml, of paden onder /share, /media, /ssl en /backup. Elk opgegeven pad en alles daaronder krijgt lees- en schrijfrechten; voer elk pad in als een apart lijstitem. Padtraversatie en andere niet-toegestane paden worden genegeerd.", + "allowed_dirs": "Paden relatief ten opzichte van de config-directory, inclusief exacte bestandsnamen zoals sensor.yaml, of paden onder /share, /media, /ssl en /backup. Aan elk item wordt lees- en schrijftoegang verleend voor dat pad en alles daaronder; voer elk pad in als een apart lijstitem. Items worden letterlijk gematcht; jokers zoals * worden niet ondersteund. Directory-traversal en andere niet-toegestane paden worden geweigerd.", "extra_yaml_keys": "Top-level sleutels die ha_config_set_yaml mag schrijven naast de ingebouwde, voor YAML-first integraties op deze installatie (bijv. alert2). Trust-boundary sleutels worden genegeerd." } }, diff --git a/custom_components/ha_mcp_tools/translations/pl.json b/custom_components/ha_mcp_tools/translations/pl.json index 2aad9fc3..76fd7631 100644 --- a/custom_components/ha_mcp_tools/translations/pl.json +++ b/custom_components/ha_mcp_tools/translations/pl.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Dodatkowe klucze zapisu YAML" }, "data_description": { - "allowed_dirs": "Ścieżki względne do katalogu konfiguracyjnego, w tym dokładne nazwy plików, takie jak sensor.yaml, lub ścieżki w katalogach /share, /media, /ssl i /backup. Każdy wpis otrzymuje uprawnienia do odczytu i zapisu dla danej ścieżki oraz wszystkiego, co się w niej znajduje; wpisz każdą ścieżkę jako oddzielny element listy. Przechodzenie w górę drzewa katalogów (traversal) i inne niedozwolone ścieżki są odrzucane.", + "allowed_dirs": "Ścieżki względem katalogu konfiguracyjnego, w tym dokładne nazwy plików, takie jak sensor.yaml, lub ścieżki w katalogach /share, /media, /ssl i /backup. Każdy wpis otrzymuje uprawnienia do odczytu i zapisu dla tej ścieżki oraz wszystkiego, co się w niej znajduje; wpisz każdą ścieżkę jako osobny element listy. Wpisy są dopasowywane dosłownie; symbole wieloznaczne, takie jak *, nie są obsługiwane. Przechodzenie w górę drzewa katalogów (traversal) i inne niedozwolone ścieżki są odrzucane.", "extra_yaml_keys": "Klucze najwyższego poziomu, które ha_config_set_yaml może zapisywać oprócz wbudowanych, dla integracji opartych na YAML w tej instalacji (np. alert2). Klucze graniczne zaufania są odrzucane." } }, diff --git a/custom_components/ha_mcp_tools/translations/ru.json b/custom_components/ha_mcp_tools/translations/ru.json index bd46464e..399b6875 100644 --- a/custom_components/ha_mcp_tools/translations/ru.json +++ b/custom_components/ha_mcp_tools/translations/ru.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Дополнительные YAML-ключи для записи" }, "data_description": { - "allowed_dirs": "Пути относительно каталога конфигурации, включая точные имена файлов, такие как sensor.yaml, или пути в директориях /share, /media, /ssl и /backup. Для каждого элемента предоставляется доступ на чтение и запись к самому пути и всему, что находится в нем; вводите каждый путь как отдельный элемент списка. Обход каталогов и другие запрещенные пути отбрасываются.", + "allowed_dirs": "Пути относительно каталога конфигурации, включая точные имена файлов, такие как sensor.yaml, или пути в каталогах /share, /media, /ssl и /backup. Для каждого элемента предоставляются права на чтение и запись для указанного пути и всего, что находится внутри него; каждый путь указывается как отдельный элемент списка. Элементы сопоставляются точным совпадением; подстановочные знаки, такие как *, не поддерживаются. Обход каталогов и другие запрещенные пути отклоняются.", "extra_yaml_keys": "Верхнеуровневые ключи, которые ha_config_set_yaml может записывать вдобавок к встроенным, — для YAML-ориентированных интеграций на этой установке (например, alert2). Ключи границы доверия отбрасываются." } }, diff --git a/custom_components/ha_mcp_tools/translations/sv.json b/custom_components/ha_mcp_tools/translations/sv.json index 40edb5ca..88d137db 100644 --- a/custom_components/ha_mcp_tools/translations/sv.json +++ b/custom_components/ha_mcp_tools/translations/sv.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "Extra YAML-skrivnycklar" }, "data_description": { - "allowed_dirs": "Sökvägar relativt konfigurationskatalogen, inklusive exakta filnamn som sensor.yaml, eller sökvägar under /share, /media, /ssl och /backup. Varje post ges läs- och skrivbehörighet för den sökvägen och allt under den; ange varje sökväg som ett separat listobjekt. Sökordsbläddring (traversal) och andra otillåtna sökvägar tas bort.", + "allowed_dirs": "Sökvägar relativa till konfigurationskatalogen, inklusive exakta filnamn som sensor.yaml, eller sökvägar under /share, /media, /ssl och /backup. Varje post ges läs- och skrivbehörighet för den sökvägen och allt under den; ange varje sökväg som ett separat listobjekt. Poster matchas bokstavligt; jokertecken som * stöds inte. Sökvägsöverskridningar och andra otillåtna sökvägar ignoreras.", "extra_yaml_keys": "Toppnivånycklar som ha_config_set_yaml får skriva utöver de inbyggda, för YAML-först-integrationer på denna installation (t.ex. alert2). Nycklar för förtroendegränser ignoreras." } }, diff --git a/custom_components/ha_mcp_tools/translations/zh-Hans.json b/custom_components/ha_mcp_tools/translations/zh-Hans.json index 3128ab5d..621a39b1 100644 --- a/custom_components/ha_mcp_tools/translations/zh-Hans.json +++ b/custom_components/ha_mcp_tools/translations/zh-Hans.json @@ -33,7 +33,7 @@ "extra_yaml_keys": "额外的 YAML 可写键" }, "data_description": { - "allowed_dirs": "相对于配置目录的路径,包括诸如 sensor.yaml 的确切文件名,或 /share、/media、/ssl 和 /backup 下的路径。每个条目均被授予对该路径及其中所有内容的读写权限;每个路径请作为一个单独的列表项输入。将丢弃目录遍历及其他不允许的路径。", + "allowed_dirs": "相对于 config 目录的路径,包括诸如 sensor.yaml 的准确文件名,或 /share、/media、/ssl 和 /backup 下的路径。每个条目都会被授予对该路径及其中任何内容的读写权限;请输入每个路径作为单独的列表项。条目按字面量匹配,不支持诸如 * 的通配符。路径遍历和其他不允许的路径将被丢弃。", "extra_yaml_keys": "除内置键外,ha_config_set_yaml 还可写入的顶层键,适用于本安装中以 YAML 为主的集成(例如 alert2)。信任边界键会被丢弃。" } }, diff --git a/custom_components/ha_mcp_tools/translations/zh-Hant.json b/custom_components/ha_mcp_tools/translations/zh-Hant.json new file mode 100644 index 00000000..6bf36f9b --- /dev/null +++ b/custom_components/ha_mcp_tools/translations/zh-Hant.json @@ -0,0 +1,168 @@ +{ + "config": { + "step": { + "user": { + "title": "HA-MCP 自訂元件", + "description": "選擇要新增的項目。**HA-MCP Server** 會在 Home Assistant 內部執行完整的 ha-mcp 伺服器,並透過 Home Assistant Webhook 對外暴露——這是多數人想要的安裝方式。它是獨立伺服器,可完全取代所有其他安裝方法:App(附加元件)、Docker、uvx/PyPI、stdio;若執行了此項,請勿同時執行其中任一其他方式。**HA-MCP File & YAML Tools** 有兩種角色。如果您的 ha-mcp 伺服器是在 Home Assistant 外部執行(app、Docker、uvx/PyPI、stdio),此項目可讓該伺服器獲得無法從外部取得的處理程序內元件功能(快速的處理程序內搜尋、設定項目後設資料、登錄檔支援的讀取),因此非常值得新增至每個外部安裝中,它同時也是 ha-mcp 選擇性啟用的檔案/YAML 工具所需的具特權檔案與 YAML 編輯服務之閘道。如果您改為新增 **HA-MCP Server** 項目,該項目已提供這些共用功能,因此只有在需要作為選擇性檔案/YAML 工具的閘道時,才需要這個第二個項目。它適用於所有伺服器類型,且隨時都能在之後新增。", + "menu_options": { + "server": "HA-MCP Server(建議)", + "tools": "HA-MCP File & YAML Tools(選用)" + } + }, + "tools": { + "title": "HA-MCP File & YAML Tools", + "description": "設定具特權的檔案與 YAML 設定服務,這些服務受限於 ha-mcp 選擇性啟用的檔案/YAML 編輯工具(功能旗標,預設為關閉)之閘道保護——這適用於所有伺服器類型,包含處理程序內的 HA-MCP Server。如果您的 ha-mcp 伺服器在 Home Assistant 外部執行,此項目還能讓它獲得無法從外部取得的處理程序內元件功能(快速的處理程序內搜尋、設定項目後設資料、登錄檔支援的讀取),即使未開啟檔案/YAML 工具也很值得擁有。您可以隨時新增或移除此項目。" + }, + "server": { + "title": "HA-MCP Server", + "description": "這會在 Home Assistant 內部執行完整的 ha-mcp 伺服器,並透過 Home Assistant Webhook 將其遠端暴露(可透過 Nabu Casa 或任何反向代理存取)。它是獨立伺服器,可完全取代 app(附加元件)、Docker、uvx/PyPI 與 stdio 安裝方法——請勿與其他 ha-mcp 伺服器同時執行。選取**提交**以啟動;您隨後可在整合選項中變更連接埠、繫結與驗證。" + } + }, + "abort": { + "already_configured": "此項目已經設定過了。", + "unsupported_home_assistant": "處理程序內的 HA-MCP Server 需要 Home Assistant {required} 或更新版本,但此執行個體目前執行的版本為 {installed}。您仍然可以在以 app(附加元件)或 Docker 容器方式執行的外部 ha-mcp 伺服器上,使用此元件的 HA-MCP File & YAML Tools 項目。" + } + }, + "options": { + "step": { + "tools_info": { + "title": "HA-MCP File & YAML Tools", + "description": "設定 ha-mcp 選擇性啟用的檔案/YAML 編輯工具所使用的具特權檔案與 YAML 編輯服務。這些設定也會顯示在 ha-mcp 伺服器的自身的設定 UI 中,並立即生效而無需重新啟動。無法覆寫的拒絕底線仍會阻擋敏感路徑(例如 .storage)與信任邊界金鑰(homeassistant、http、frontend、lovelace)。", + "data": { + "allowed_dirs": "額外檔案路徑", + "extra_yaml_keys": "額外的 YAML 寫入金鑰" + }, + "data_description": { + "allowed_dirs": "相對於 config 目錄的路徑,包含精確的檔名(例如 sensor.yaml),或是 /share、/media、/ssl 與 /backup底下的路徑。每個項目都會被授予該路徑及其下方所有內容的讀寫權限;請將每個路徑輸入為獨立的列表項目。項目會以字面值進行比對;不支援如 * 的萬用字元。會略過目錄普遍遍歷與其他不允許的路徑。", + "extra_yaml_keys": "除了內建金鑰之外,ha_config_set_yaml 可以寫入的頂層金鑰,適用於此安裝上的 YAML 優先整合(例如 alert2)。信任邊界金鑰將被略過。" + } + }, + "init": { + "title": "HA-MCP Server", + "description": "設定 HA-MCP 伺服器。{panel_hint}此處的變更會在儲存時套用。\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}", + "data": { + "channel": "發布管道", + "auto_update": "自動伺服器更新", + "server_port": "MCP 伺服器監聽連接埠", + "bind_host": "網路存取", + "webhook_auth": "驗證模式", + "oauth_client_id_override": "舊版 OAuth:自訂用戶端 ID(選用)", + "oauth_client_secret_override": "舊版 OAuth:自訂用戶端密鑰(選用)", + "oauth_regenerate": "舊版 OAuth:立即重新產生用戶端 ID/密鑰", + "pip_spec": "開發人員:ha-mcp 套件覆寫", + "server_url": "Home Assistant URL(進階)", + "external_url": "外部 URL(選用)", + "webhook_id_override": "自訂 Webhook 密鑰(選用)", + "secret_path_override": "自訂直接存取路徑(選用)", + "regenerate_secrets": "立即重新產生連線密鑰", + "enable_webhook": "透過 Webhook 進行遠端存取", + "enable_llm_api": "對話代理 LLM API", + "llm_api_exposure": "對話代理工具暴露", + "enable_startup_notification": "啟動通知", + "enable_sidebar_panel": "側邊欄設定面板" + }, + "data_description": { + "channel": "「穩定版 (Stable)」會安裝最新的穩定發布版本;「開發版 (Development)」會安裝最新的開發建置版本。當啟用自動更新時,重新載入或重新啟動,加上定期檢查,將會安裝所選管道的最新建置版本。下方覆寫的開發人員套件具有較高優先權,並會停用自動更新。", + "auto_update": "啟用時,將會自動安裝所選管道的最新發布版本——於重新載入或重新啟動時,以及透過定期檢查。關閉時,伺服器將維持在目前已安裝的版本,直到您重新開啟此功能為止。這僅控制 ha-mcp 伺服器套件;HA-MCP 自訂元件本身的更新依然透過 HACS 進行。", + "server_port": "此伺服器監聽的連接埠。ha-mcp App(附加元件)使用 9583,因此預設為 9584,讓兩者可同時執行;若未執行該 App(附加元件),可使用任何未占用的連接埠。", + "bind_host": "誰可以直接連線至 MCP 伺服器連接埠。預設值符合 app(附加元件):可在您的本機網路上存取,並以密鑰路徑作為憑證。選擇「回送 (loopback)」以僅允許來自 Home Assistant 電腦本身的連線——無論哪種方式,Webhook URL 與側邊欄面板都能正常運作。", + "webhook_auth": "MCP 用戶端如何在 Webhook URL 證明其身分。使用密鑰 URL 時,連結本身就是唯一的憑證——OAuth 仍會為使用它的用戶端執行,但每個登入都會自動核准:不需要登入,也不需要用戶端 ID 或密鑰。使用 Home Assistant 登入時,像 claude.ai 這類的用戶端會使用 Home Assistant 管理員帳號 (OAuth) 登入。使用舊版 OAuth 時,此整合會執行自己的登入服務,並發行用戶端 ID 與密鑰,以貼到需要它們的用戶端中——可能需要重新啟動 Home Assistant 才能完全啟用或停用它。", + "oauth_client_id_override": "取代自動產生的舊版 OAuth 用戶端 ID。保持空白以保留目前的值。僅在驗證模式設為舊版 OAuth 時使用。", + "oauth_client_secret_override": "取代自動產生的舊版 OAuth 用戶端密鑰。保持空白以保留目前的值。僅在驗證模式設為舊版 OAuth 時使用。", + "oauth_regenerate": "單次動作:為舊版 OAuth 模式產生全新的用戶端 ID(Client ID)與密鑰(Secret)。它只會在修復提示要求的 Home Assistant 重新啟動後生效——在重新啟動之前,先前的用戶端 ID 與密鑰會繼續運作,新的則不會。同時也會清除上方的兩個覆寫欄位。", + "pip_spec": "保持空白。僅用於測試特定的 ha-mcp 建置(例如預先發布版本釘選);會覆寫發布管道並停用自動更新,直到清除為止。", + "server_url": "處理程序內伺服器用來連線至 Home Assistant 的 URL(通常就是此執行個體)。留空會依此執行個體的連接埠與 SSL 設定自動推導;只有在伺服器必須改走其他路由時才需設定。", + "external_url": "顯示為主要的連線 URL ——當 Home Assistant 位於您自己的網域或反向代理背後時請使用此項。請輸入包含通訊協定的完整基礎位址。它必須直接指向 Home Assistant ——在瀏覽器中開啟它應該能到達您的 HA 登入頁面——且不得包含如 :8123(或任何其他連接埠)的連接埠,否則遠端 MCP 用戶端將無法連線。留空以自動使用 Nabu Casa / 本機位址。", + "webhook_id_override": "取代連線 URL (/api/webhook/...) 中的隨機 webhook 密鑰。URL 即為憑證——請使用長且難以猜測的值。留空以保留目前的值。", + "secret_path_override": "取代用於伺服器連接埠上直接存取的隨機路徑。相同的規則:路徑即為憑證。留空以保留目前的值。", + "regenerate_secrets": "單次動作:產生全新的隨機 webhook 密鑰與直接存取路徑,並立即使舊的連線 URL 失效。同時也會清除上方的兩個覆寫欄位。", + "enable_webhook": "為僅限本機模式關閉:完全不會註冊 Home Assistant webhook,因此任何事物(包含 Nabu Casa)都無法透過 Home Assistant 連線至伺服器。直接伺服器連接埠與側邊欄面板將繼續運作。", + "enable_llm_api": "提供完整工具集給 Home Assistant 對話代理程式(OpenAI、Google、Ollama 等):啟用時,代理程式可在「控制 Home Assistant」下方選取「HA-MCP Server」,並從 Assist 聊天與語音中驅動工具。啟用只會使其可被選取——在您於代理程式上挑選它之前,不會暴露任何內容。使用指南:{llm_api_docs_url}", + "llm_api_exposure": "提供給對話代理程式的工具集外觀。工具搜尋(預設)可保持代理程式的上下文簡短:具有釘選工具加上搜尋/執行中介工具的精簡 API。完整目錄直接列出每個暴露的工具——較適合大上下文模型。兩者會並排註冊,以便每個代理程式在「控制 Home Assistant」下方挑選自己的工具。個別工具暴露管理可在 HA-MCP 設定面板中進行;詳細資訊:{llm_api_docs_url}", + "enable_startup_notification": "每次伺服器啟動時顯示通知,指向僅限管理員的設定介面。關閉以安靜地啟動——連線 URL 仍會顯示在 Home Assistant 記錄檔中。", + "enable_sidebar_panel": "在側邊欄中顯示 HA-MCP 設定面板(僅限管理員)。關閉以移除側邊欄項目——伺服器選項仍可在此畫面上使用。" + } + } + } + }, + "issues": { + "server_start_failed": { + "title": "HA-MCP 處理程序內伺服器啟動失敗", + "description": "無法在 Home Assistant 內啟動 HA-MCP 處理程序內伺服器:\n\n{detail}\n\n請檢查 Home Assistant 記錄檔,然後重新載入整合(或修正根本問題並重新載入)以重試。" + }, + "server_package_install_failed": { + "title": "無法安裝 HA-MCP 處理程序內伺服器套件", + "description": "安裝用於處理程序內伺服器的 ha-mcp 套件失敗:\n\n{detail}\n\n請解決上述描述的相容性或安裝問題,然後重新載入整合以重試。" + }, + "component_outdated": { + "title": "透過 HACS 更新 HA-MCP 自訂元件", + "description": "已安裝的 ha-mcp 伺服器需要 HA-MCP 自訂元件 {required} 或更新版本,但您目前安裝的是 {installed}。請透過 HACS 更新元件(開啟 HA-MCP 自訂元件項目,若尚未顯示更新則使用「更新資訊」)並重新整理 Home Assistant。伺服器在此期間會繼續執行,但在更新元件之前,某些較新功能可能無法運作。" + }, + "server_update_held": { + "title": "HA-MCP 伺服器更新正在等待元件更新", + "description": "ha-mcp 伺服器 {latest} 可供使用,但該版本同時也更新了 HA-MCP 自訂元件(至 {shipped};您目前執行的是 {running})。為避免啟動執行中元件從未測試過的伺服器版本,自動伺服器更新將暫緩,直到元件更新為止。\n\n請透過 HACS 更新元件(開啟 HA-MCP 自訂元件項目,若尚未顯示更新則使用「更新資訊」),然後重新啟動 Home Assistant——伺服器更新隨後會自動安裝。若無論如何都要安裝伺服器更新,請按下 HA-MCP 伺服器更新實體上的「安裝」。" + }, + "legacy_hacs_source": { + "title": "從舊版儲存庫安裝的元件", + "description": "HACS 正在追蹤此元件的主要 ha-mcp 伺服器儲存庫,因此 HACS 在此顯示的是伺服器的版本號碼與發布說明,而非元件本身的版本(2.x)。更新會繼續運作,但標籤會維持錯誤狀態。若要修正:從 HACS 中移除此儲存庫(您的整合設定與設定項目會被保留),將 homeassistant-ai/ha-mcp-integration 新增為自訂儲存庫,從中重新安裝元件,然後重新啟動 Home Assistant。" + }, + "legacy_oauth_restart": { + "title": "重新啟動 Home Assistant 以套用舊版 OAuth 設定", + "fix_flow": { + "step": { + "confirm": { + "title": "要重新啟動 Home Assistant 嗎?", + "description": "送出此表單將會重新啟動 Home Assistant 並套用舊版 OAuth 設定。\n\n按一下**送出**以立即重新啟動。" + } + } + } + } + }, + "selector": { + "server_channel": { + "options": { + "stable": "穩定版(建議)", + "dev": "開發版(最新建置)" + } + }, + "server_webhook_auth": { + "options": { + "none": "機密 webhook URL(預設)", + "ha_auth": "使用 Home Assistant 登入(OAuth)", + "legacy": "舊版 OAuth(此整合會發行要貼上的用戶端 ID/密鑰)" + } + }, + "llm_api_exposure": { + "options": { + "tool_search": "工具搜尋(精簡,預設)", + "full": "完整目錄", + "both": "兩者(依每個代理程式選擇)" + } + } + }, + "entity": { + "update": { + "server_update": { + "name": "更新" + } + } + }, + "common": { + "connect_direct_access": "來自 Home Assistant 機器的直接存取:{url}", + "connect_local_lan": "本機/區域網路 (當網路存取為 \"Local network\"):{url}", + "connect_remote_url": "遠端連線 URL:{url}", + "connect_urls_label": "連線 URL:", + "connect_urls_pending": "伺服器啟動後,連線 URL 將會顯示在此處(以及 Home Assistant 記錄檔中)。", + "connect_webhook_disabled": "透過 webhook 的遠端存取已停用(僅限本機模式)。", + "oauth_creds_pending": "伺服器啟動後,用戶端 ID 與用戶端密鑰將會顯示在此處。", + "oauth_not_serving": "舊版 OAuth 尚未提供這些憑證;請在 Home Assistant 提示時重新啟動以啟用。", + "oauth_select_legacy_mode": "將驗證模式設為上方的舊版 OAuth 並儲存,以產生用戶端 ID 與用戶端密鑰。", + "panel_hint": "開啟 [HA-MCP 設定面板](/ha-mcp)進行工具管理與伺服器設定。", + "tools_module_installed": "Beta/進階檔案與 YAML 工具模組(選用):已安裝", + "tools_module_not_installed": "Beta/進階檔案與 YAML 工具模組(選填):未安裝 — 請在此整合頁面上按一下「新增項目」並選擇 \"HA-MCP File & YAML Tools\" 來加以新增", + "tools_module_not_loaded": "Beta/進階檔案與 YAML 工具模組(選填):已安裝但未載載 — 請在此整合頁面上啟用或重新載入「HA-MCP File & YAML Tools」項目", + "version_line": "元件 {component_version} - 伺服器 ha-mcp {server_version}({channel} 通道)", + "version_not_installed": "尚未安裝", + "version_unknown": "未知" + } +} diff --git a/custom_components/ha_mcp_tools/websocket_api.py b/custom_components/ha_mcp_tools/websocket_api.py index 4dae1311..ed9be844 100644 --- a/custom_components/ha_mcp_tools/websocket_api.py +++ b/custom_components/ha_mcp_tools/websocket_api.py @@ -37,7 +37,7 @@ the info handshake carries no capability entry: is byte-identical to the REST ``/api/states/`` serialization by construction; the server maps found/missing onto its per-id error contract. * ``ha_mcp_tools/blueprint_get`` — the full body of one installed blueprint - (``{metadata, config}``), which core's ``blueprint/list`` never returns (it + (``{metadata, config, yaml}``), which core's ``blueprint/list`` never returns (it serves only ``{metadata}``). The path is jailed under ``/blueprints//`` (symlink-safe containment, mirroring the file-tool jail) and the file read + parse run off the event loop in the async @@ -335,6 +335,7 @@ WS_ENTITY_LOOKUP = f"{WS_API_PREFIX}/entity_lookup" WS_BACKUP_PREP = f"{WS_API_PREFIX}/backup_prep" WS_REGISTRIES = f"{WS_API_PREFIX}/registries" WS_DASHBOARDS = f"{WS_API_PREFIX}/dashboards" +WS_DASHBOARD_EDIT = f"{WS_API_PREFIX}/dashboard_edit" WS_SERVICES_LIST = f"{WS_API_PREFIX}/services_list" WS_REFERENCE_DATA = f"{WS_API_PREFIX}/reference_data" WS_SERVER_ENTRY = f"{WS_API_PREFIX}/server_entry" @@ -360,6 +361,11 @@ CAPABILITIES: list[str] = [ "helpers_list", "states", "blueprint_get", + # A flag on blueprint_get: gates its additive ``yaml`` result field (the raw + # on-disk blueprint text). The server only asks for the text when this is + # advertised, so an older build simply serves the parsed body and the server + # looks for the text elsewhere. + "blueprint_text", "device_get", "device_list", # A semantic flag shared by every device-registry-backed read. Components @@ -377,6 +383,7 @@ CAPABILITIES: list[str] = [ "backup_prep", "registries", "dashboards", + "dashboard_edit", # A flag, not a standalone command: gates the additive whole-document # search-result keys on ``ha_mcp_tools/dashboards`` mode=search # (``document_matches`` + ``yaml_skipped`` + ``load_failed``, issue #2008). @@ -570,9 +577,12 @@ def async_register_commands(hass: HomeAssistant) -> None: HA-core state rather than anything the unloaded entry cached, HA core authenticates the connection, and ``@require_admin`` gates each command — so a caller reaching it can already do the same through HA's own WS API. The - write commands are no exception: their D1 domain block refuses - ``domain == "ha_mcp_tools"`` unconditionally, so the leftover surface can - never reach the privileged filesystem/YAML services, which + service-dispatching writes enforce D1: they refuse + ``domain == "ha_mcp_tools"`` unconditionally. Dashboard edits do not dispatch + services: they use Core's Lovelace storage API under the same admin gate as + ``lovelace/config/save``. Server-entry updates retain their own live-entry + and option validation. These commands do not grant access to the privileged + filesystem/YAML services, which :func:`~custom_components.ha_mcp_tools._async_unload_tools_entry` does remove on unload. Admin-gated commands answering from live core state until the next restart is the trade this makes. @@ -613,6 +623,7 @@ def _command_specs() -> list[tuple[dict[Any, Any], Any, Any]]: (_backup_prep_schema(), _do_backup_prep, None), (_registries_schema(), _do_registries, None), (_dashboards_schema(), _do_dashboards, _dashboards_prep), + (_dashboard_edit_schema(), _do_dashboard_edit, _dashboard_edit_prep), (_services_list_schema(), _do_services_list, _services_list_prep), (_reference_data_schema(), _do_reference_data, None), (_server_entry_schema(), _do_server_entry, None), @@ -625,7 +636,7 @@ def _command_specs() -> list[tuple[dict[Any, Any], Any, Any]]: _do_server_entry_update, _server_entry_update_prep, ), - # The first WRITE command: the dispatch + the bounded confirmation wait are + # The service WRITE command: dispatch + the bounded confirmation wait are # inherently async, so ALL of the work lives in the ``_call_service_prep`` # async pre-step and ``_do_call_service`` is a pure response formatter. (_call_service_schema(), _do_call_service, _call_service_prep), @@ -3205,27 +3216,31 @@ def _do_blueprint_get( params: dict[str, Any], *, body: dict[str, Any] | None = None, + text: str | None = None, ) -> dict[str, Any]: - """Return one installed blueprint's full body as ``{metadata, config}``. + """Return one installed blueprint as ``{metadata, config, yaml}``. core's ``blueprint/list`` returns only ``{metadata}`` (no triggers / - conditions / actions / sequence), so the server can otherwise serve metadata - only. This reads the on-disk blueprint file and returns the parsed body: - ``config`` is the full file (the server merges it additively over the - ``blueprint/list`` metadata) and ``metadata`` is its ``blueprint:`` section. - When the file is missing, unparseable, or the requested path escapes the jail, - both come back ``None`` (the server keeps metadata-only) — see - :func:`_read_blueprint_file`. + conditions / actions / sequence, and never the file text), so the server can + otherwise serve metadata only. This reads the on-disk blueprint file once and + returns both views of it: ``config`` is the parsed file (the server merges it + additively over the ``blueprint/list`` metadata), ``metadata`` is its + ``blueprint:`` section, and ``yaml`` is the raw text the server hands back for + a round trip through ``blueprint/save``. Each is ``None`` when it could not be + produced — a file that reads but does not parse still yields its ``yaml`` — + and all three are ``None`` when the file is missing or the requested path + escapes the jail (see :func:`_read_blueprint_file`). Pure: the blocking jail-resolve + file read + YAML parse run in the executor - via :func:`_blueprint_get_prep`, which passes the parsed ``body`` in. + via :func:`_blueprint_get_prep`, which passes both views in. """ if not isinstance(body, dict): - return {"metadata": None, "config": None} + return {"metadata": None, "config": None, "yaml": text} metadata = body.get("blueprint") return { "metadata": _plainify(metadata) if isinstance(metadata, dict) else None, "config": _plainify(body), + "yaml": text, } @@ -3237,27 +3252,41 @@ async def _blueprint_get_prep( The path jail (symlink-safe ``Path.resolve`` containment), the ``open()`` and the YAML parse are all blocking filesystem work, so they run in the executor via :meth:`hass.async_add_executor_job` — keeping :func:`_do_blueprint_get` a - pure assembler over the parsed ``body`` this returns (``None`` on any failure). + pure assembler over the raw text and parsed body this returns (both ``None`` + on a failed read). """ domain = msg["domain"] path = msg["path"] - body = await hass.async_add_executor_job(_read_blueprint_file, hass, domain, path) - return {"body": body} + read = await hass.async_add_executor_job(_read_blueprint_file, hass, domain, path) + return {"body": read.body, "text": read.text} -def _read_blueprint_file( - hass: HomeAssistant, domain: str, path: str -) -> dict[str, Any] | None: - """Resolve + jail + read + parse one blueprint YAML file. ``None`` on failure. +class _BlueprintFile(NamedTuple): + """One blueprint file read once: its raw ``text`` and its parsed ``body``. + + ``text`` is ``None`` when the file could not be read at all; ``body`` is + additionally ``None`` when it read but did not parse into a mapping. + """ + + text: str | None + body: dict[str, Any] | None + + +_UNREADABLE_BLUEPRINT = _BlueprintFile(None, None) + + +def _read_blueprint_file(hass: HomeAssistant, domain: str, path: str) -> _BlueprintFile: + """Resolve + jail + read + parse one blueprint YAML file, reading it once. Blueprint files live under ``/blueprints//``. The requested ``path`` is joined under that root and resolved symlink-safe (mirrors the file-tool jail's ``_resolves_within`` — resolve the RAW input, following symlinks, THEN check containment, so ``//..`` cannot escape). A path escaping the root — via ``..``, an absolute path, or a symlink — yields - ``None`` (rejected, never opened). A missing file, a non-file target, a read - error, or a YAML parse error also yields ``None``. Only a valid, contained, - parseable blueprint returns its full parsed body. + an empty result (rejected, never opened), as does a missing file, a non-file + target, or a read error. A file that reads but does not parse into a mapping + keeps its ``text`` and drops its ``body``, so the caller can still round-trip + the exact bytes it holds. Parsed with :class:`_BlueprintLoader`: ``!input`` markers are preserved and every other custom tag (``!secret`` / ``!include`` / …) is neutralized to @@ -3267,29 +3296,32 @@ def _read_blueprint_file( config = getattr(hass, "config", None) path_fn = getattr(config, "path", None) if not callable(path_fn): - return None + return _UNREADABLE_BLUEPRINT try: base = Path(path_fn("blueprints", domain)) candidate = Path(path) if path.startswith("/") else base / path real = candidate.resolve() base_real = base.resolve() except (OSError, ValueError): - return None + return _UNREADABLE_BLUEPRINT if not (real == base_real or real.is_relative_to(base_real)): - return None + return _UNREADABLE_BLUEPRINT try: - with open(real, encoding="utf-8") as handle: - # Instance form (not yaml.load) mirrors the component's existing - # _PackagesDirLoader usage; _BlueprintLoader is a SafeLoader subclass, - # so no !!python/object can construct arbitrary types. - loader = _BlueprintLoader(handle) - try: - parsed = loader.get_single_data() - finally: - loader.dispose() - except (OSError, ValueError, yaml.YAMLError): - return None - return parsed if isinstance(parsed, dict) else None + text = real.read_text(encoding="utf-8") + except (OSError, ValueError): + return _UNREADABLE_BLUEPRINT + try: + # Instance form (not yaml.load) mirrors the component's existing + # _PackagesDirLoader usage; _BlueprintLoader is a SafeLoader subclass, + # so no !!python/object can construct arbitrary types. + loader = _BlueprintLoader(text) + try: + parsed = loader.get_single_data() + finally: + loader.dispose() + except (ValueError, yaml.YAMLError): + return _BlueprintFile(text, None) + return _BlueprintFile(text, parsed if isinstance(parsed, dict) else None) def _construct_blueprint_input(loader: Any, node: Any) -> dict[str, str]: @@ -6803,3 +6835,29 @@ def _dispatched_unconfirmed_bulk_result( "dispatched": sum(1 for r in op_results if r["dispatched"]), "failed": sum(1 for r in op_results if r.get("error") is not None), } + + +def _dashboard_edit_schema() -> dict[Any, Any]: + """The additive edit command; cross-field validation precedes any save.""" + return { + vol.Required("type"): WS_DASHBOARD_EDIT, + vol.Optional("url_path"): vol.Any(str, None), + vol.Optional("expected_hash"): vol.Any(str, None), + vol.Optional("config"): dict, + vol.Optional("patch"): list, + } + + +async def _dashboard_edit_prep( + hass: HomeAssistant, msg: dict[str, Any] +) -> dict[str, Any]: + from .dashboard_edit import async_edit_dashboard + + return {"result": await async_edit_dashboard(hass, msg)} + + +def _do_dashboard_edit( + hass: HomeAssistant, msg: dict[str, Any], *, result: dict[str, Any] +) -> dict[str, Any]: + """Preserve the write outcome assembled by the async edit lifecycle.""" + return result diff --git a/custom_components/ha_washdata/__init__.py b/custom_components/ha_washdata/__init__.py index 57f6e07f..705d7be0 100644 --- a/custom_components/ha_washdata/__init__.py +++ b/custom_components/ha_washdata/__init__.py @@ -28,7 +28,7 @@ from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ServiceValidationError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr from .const import ( @@ -112,6 +112,21 @@ from .const import ( from .log_utils import DeviceLoggerAdapter from .options_utils import strip_null_options +try: # pragma: no cover - present in every supported HA, guarded on principle + from homeassistant.setup import SetupPhases, async_pause_setup +except ImportError: # pragma: no cover + from contextlib import nullcontext + + class SetupPhases: # type: ignore[no-redef] + """Fallback so a missing helper degrades to plain (uncredited) waiting.""" + + WAIT_IMPORT_PACKAGES = "wait_import_packages" + + def async_pause_setup(hass, phase): # type: ignore[misc] + """No-op stand-in for HA's setup-time credit context manager.""" + return nullcontext() + + _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [ @@ -121,6 +136,15 @@ PLATFORMS: list[Platform] = [ Platform.BUTTON, ] +# Shared future for the once-per-HA-instance ML module warm-up (issue #408). +ML_PRELOAD_FUTURE_KEY = "ha_washdata_ml_preload" + +# Entry ids whose entity platforms are currently forwarded. HA leaves forwarded +# platforms in place when async_setup_entry raises, and it refuses to set the same +# platform up twice, so a retry after a part-way failure has to take them down +# first (issue #425). +FORWARDED_ENTRIES_KEY = "ha_washdata_forwarded_entries" + def _require_str(value: Any, name: str) -> str: if not isinstance(value, str) or not value: @@ -461,6 +485,20 @@ async def _async_preload_ml_modules(hass: HomeAssistant) -> None: Warming the module cache once per setup in the import executor makes every later resolution a ``sys.modules`` lookup. Best effort: a failure here only means ML stays inert, so it must never block setup. + + Two things matter for startup time here (issue #408), because + ``hass.import_executor`` is ``max_workers=1`` and is shared with every other + integration importing during startup, so an awaited job on it costs however + deep that queue happens to be - measured at 35-95 s in real user + diagnostics, against ~1 ms of actual work: + + 1. The job is coalesced onto ONE shared future for the whole HA instance. + ``preload_models()`` is idempotent, so a second job per config entry + would buy nothing but another full trip through that queue. + 2. The wait is wrapped in ``async_pause_setup(WAIT_IMPORT_PACKAGES)``, which + is how HA core reports its own heavy imports (``workday``, ``holiday``, + ``stream``, ``mqtt``, ...): the queue wait is credited back instead of + being billed to us as "Integration startup time". """ def _preload() -> None: @@ -469,24 +507,216 @@ async def _async_preload_ml_modules(hass: HomeAssistant) -> None: preload_models() + future = hass.data.get(ML_PRELOAD_FUTURE_KEY) + if future is None: + future = hass.data[ML_PRELOAD_FUTURE_KEY] = hass.async_add_import_executor_job( + _preload + ) + try: - await hass.async_add_import_executor_job(_preload) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # shield: a cancelled entry setup must not cancel the warm-up that + # the other entries are waiting on. + await asyncio.shield(future) except Exception: # pylint: disable=broad-exception-caught _LOGGER.debug("ML module preload failed", exc_info=True) + # Broken install: drop the memo so a later setup/reload retries instead + # of every entry re-raising the one cached failure forever. A cancelled + # setup is not caught here (CancelledError is not an Exception), so the + # memo correctly survives for the entries still awaiting the warm-up. + if hass.data.get(ML_PRELOAD_FUTURE_KEY) is future: + hass.data.pop(ML_PRELOAD_FUTURE_KEY, None) + + +async def _async_setup_shared( + hass: HomeAssistant, log: DeviceLoggerAdapter +) -> None: + """Register everything that belongs to the HA instance, not to one appliance. + + The custom card, the sidebar panel, the WebSocket API, the conversation + intents and the cached integration version are global: the first config + entry brings them up and every later entry is a guarded no-op. + + Issue #425: this used to sit near the END of ``async_setup_entry``, so any + earlier per-appliance failure took the whole install's UI down with it - and + because the panel is where almost every setting is edited (including the + ``via_device`` link that aborted setup in #418), losing it left the user with + no way to undo the setting that broke setup. Registering it first makes the + panel independent of whether any individual appliance sets up. + + Failures inside are already handled per block (the card defers and retries, + the panel logs and retries on the next setup), so a missing UI never fails + the entry. + """ + # Register custom card via frontend.py - once per HA instance only. + if not hass.data.get("ha_washdata_card_registered") and not hass.data.get( + "ha_washdata_card_deferred" + ) and not hass.data.get("ha_washdata_card_registering"): + # pylint: disable=import-outside-toplevel + from .frontend import ( + CARD_REGISTERED, + CARD_DEFERRED, + WashDataCardRegistration, + ) + + card_reg = WashDataCardRegistration(hass) + hass.data["ha_washdata_card_registering"] = True + try: + register_result = await card_reg.async_register() + except Exception as err: # pylint: disable=broad-exception-caught + log.warning("Card registration failed, will retry on next setup: %s", err) + else: + if register_result == CARD_REGISTERED: + hass.data["ha_washdata_card_deferred"] = False + hass.data["ha_washdata_card_registered"] = True + elif register_result == CARD_DEFERRED: + hass.data["ha_washdata_card_deferred"] = True + hass.data["ha_washdata_card_registered"] = False + else: + hass.data["ha_washdata_card_deferred"] = False + hass.data["ha_washdata_card_registered"] = False + # The reason used to be debug-only inside frontend.py, so this + # warning told the user something was wrong but not what (#432). + log.warning( + "Card registration failed and was not deferred: %s", + getattr(card_reg, "last_failure_reason", "unknown"), + ) + finally: + # finally, not one reset per branch: `except Exception` does not catch + # CancelledError, and HA cancels entry setup on timeout or on a reload + # racing it. A flag left True is never cleared again, so every later + # setup skips card registration until HA restarts. + hass.data["ha_washdata_card_registering"] = False + + # Register full-screen sidebar panel - once per HA instance only. + # pylint: disable=import-outside-toplevel + from .frontend import async_register_panel, PANEL_REGISTERED_KEY + + if not hass.data.get(PANEL_REGISTERED_KEY): + await async_register_panel(hass) + + # Register WebSocket API commands for the panel. Re-run on every setup/reload: + # HA's async_register_command overwrites the handler per command type, so this + # is idempotent AND means NEW commands become available after an integration + # reload, not only after a full Home Assistant restart (previously the + # once-per-instance guard forced a full restart for any newly-added command). + from .ws_api import ( # pylint: disable=import-outside-toplevel + async_load_panel_config, + async_register_commands, + ) + + await async_load_panel_config(hass) # self-guards; safe to call repeatedly + from . import store_account # pylint: disable=import-outside-toplevel + await store_account.async_load(hass) # integration-wide online flag + account + + # Cache the integration version from HA's already-loaded manifest (no file IO). + # ws_get_constants reads it from hass.data; we can't do a module-level read_text + # in ws_api.py because the module is imported lazily inside this coroutine and the + # IO runs on the event loop (#328/#335). + if "ha_washdata_version" not in hass.data: + try: + from homeassistant.loader import async_get_integration as _aget_integration # pylint: disable=import-outside-toplevel + _integ = await _aget_integration(hass, DOMAIN) + hass.data["ha_washdata_version"] = _integ.manifest.get("version", "") or "" + except Exception as err: # pylint: disable=broad-exception-caught + _LOGGER.debug("Could not load ha_washdata version from manifest: %s", err) + # Fall back to reading manifest.json off the event loop. Only + # write the key when the read succeeds so that a double failure + # (loader + file) leaves the key absent rather than storing "" + # (which would shadow _INTEGRATION_VERSION in ws_get_constants). + def _read_manifest_version() -> str: + try: + return json.loads( + (Path(__file__).parent / "manifest.json").read_text(encoding="utf-8") + ).get("version") or "" + except Exception: # pylint: disable=broad-exception-caught + return "" + _fallback_version = await hass.async_add_executor_job(_read_manifest_version) + if _fallback_version: + hass.data["ha_washdata_version"] = _fallback_version + + async_register_commands(hass) + hass.data["ha_washdata_ws_registered"] = True + + # Register conversation intents (e.g. "is my washer done?") - once per HA + # instance. Intents are domain-global, so guard against re-registration when + # more than one device is configured. + if not hass.data.get("ha_washdata_intents_registered"): + from .intents import async_setup_intents # pylint: disable=import-outside-toplevel + + async_setup_intents(hass) + hass.data["ha_washdata_intents_registered"] = True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WashData from a config entry.""" _log = DeviceLoggerAdapter(_LOGGER, entry.title) - # Guard against duplicate setup during hot-reload - if entry.entry_id in hass.data.get(DOMAIN, {}): - _log.warning( - "Entry %s already set up, skipping duplicate setup", entry.entry_id - ) - return True - hass.data.setdefault(DOMAIN, {}) + # Panel / card / WebSocket API first: they belong to the HA instance, not to + # this appliance, so they must not depend on this entry getting through + # (#425). See _async_setup_shared. + await _async_setup_shared(hass, _log) + + # Debris from an earlier setup that failed part-way. HA does NOT call + # async_unload_entry for an entry that never reached LOADED, so the manager + # this function stores before the failure point is still here - and before + # #425 we answered that by logging "already set up" and returning True, + # which told HA the entry was loaded while everything after the failure + # point (device link, half the services, the update listener) had never + # run. Every later reload repeated the lie, so the only way out was to + # delete and re-add the appliance. Throw the leftover away and set up for + # real instead. + stale = hass.data[DOMAIN].pop(entry.entry_id, None) + if stale is not None: + _log.warning( + "Entry %s still holds a manager from an earlier incomplete setup; " + "discarding it and setting up again", + entry.entry_id, + ) + try: + await stale.async_shutdown() + except Exception: # pylint: disable=broad-exception-caught + _log.debug("Shutdown of the stale manager failed", exc_info=True) + + # Entity platforms from an earlier attempt are still registered (HA leaves + # forwarded platforms in place when setup fails); forwarding them again raises + # "has already been setup", so take them down first. + # + # NOT gated on the stale manager: FORWARDED_ENTRIES_KEY is the independent + # record of a forward, and the two can part company. async_reload_entry's + # full-reload branch runs precisely when the manager is missing - it calls + # async_unload_entry, which leaves the marker in place when the platform unload + # is refused, and then calls this function with nothing to find. Gating the + # cleanup on `stale` skipped it in exactly the case it exists for. + if entry.entry_id in hass.data.get(FORWARDED_ENTRIES_KEY, set()): + unloaded = False + try: + unloaded = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS + ) + except Exception: # pylint: disable=broad-exception-caught + _log.debug("Unloading the stale platforms failed", exc_info=True) + # Only forget the platforms once they are actually gone. A platform that + # refuses to unload leaves them forwarded, and dropping the record here + # would make the next attempt skip the unload and hit "has already been + # setup" forever - the #425 loop this set exists to break. + # + # Setup deliberately CARRIES ON when the unload does not succeed, rather + # than returning False. `False` does not mean "a live platform refused": + # HA's ConfigEntry.async_unload catches a never-loaded platform's + # ValueError("Config entry was never loaded!"), logs "Error unloading entry + # X for sensor" and returns False (`config_entries.py:997-1015`). Since the + # marker is written BEFORE the forward on purpose, the common instance of + # False is "the forward set nothing up", where there is nothing registered + # to collide with and the retry succeeds. Aborting on False would abort + # every later attempt identically - a permanent brick, the exact #425 loop + # this set exists to break, and it is what + # `test_reload_after_a_failed_setup_really_sets_up` and + # `test_an_unloadable_stale_platform_does_not_abort_the_retry` pin. + if unloaded: + hass.data[FORWARDED_ENTRIES_KEY].discard(entry.entry_id) + # Warm the ML module cache before anything can score in the event loop. await _async_preload_ml_modules(hass) @@ -512,6 +742,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await manager.async_setup() await _migrate_online_to_global(hass, entry, manager) + # Recorded BEFORE the await on purpose: async_forward_entry_setups gathers the + # platform setups, so one raising leaves the others set up, and a marker written + # afterwards would never be reached. The next attempt would then skip the unload + # and forward an already-registered platform (#425). Marking a forward that in + # fact set nothing up is harmless: the unload of a never-loaded platform raises, + # that is caught, and the retry forwards normally. + hass.data.setdefault(FORWARDED_ENTRIES_KEY, set()).add(entry.entry_id) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) _apply_device_link(hass, entry) @@ -718,95 +955,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_register(DOMAIN, "trim_cycle", handle_trim_cycle) - # Register custom card via frontend.py - once per HA instance only. - if not hass.data.get("ha_washdata_card_registered") and not hass.data.get( - "ha_washdata_card_deferred" - ) and not hass.data.get("ha_washdata_card_registering"): - # pylint: disable=import-outside-toplevel - from .frontend import ( - CARD_REGISTERED, - CARD_DEFERRED, - WashDataCardRegistration, - ) - - card_reg = WashDataCardRegistration(hass) - hass.data["ha_washdata_card_registering"] = True - try: - register_result = await card_reg.async_register() - except Exception as err: # pylint: disable=broad-exception-caught - hass.data["ha_washdata_card_registering"] = False - _log.warning("Card registration failed, will retry on next setup: %s", err) - else: - hass.data["ha_washdata_card_registering"] = False - if register_result == CARD_REGISTERED: - hass.data["ha_washdata_card_deferred"] = False - hass.data["ha_washdata_card_registered"] = True - elif register_result == CARD_DEFERRED: - hass.data["ha_washdata_card_deferred"] = True - hass.data["ha_washdata_card_registered"] = False - else: - hass.data["ha_washdata_card_deferred"] = False - hass.data["ha_washdata_card_registered"] = False - _log.warning("Card registration failed and was not deferred") - - # Register full-screen sidebar panel - once per HA instance only. - # pylint: disable=import-outside-toplevel - from .frontend import async_register_panel, PANEL_REGISTERED_KEY - - if not hass.data.get(PANEL_REGISTERED_KEY): - await async_register_panel(hass) - - # Register WebSocket API commands for the panel. Re-run on every setup/reload: - # HA's async_register_command overwrites the handler per command type, so this - # is idempotent AND means NEW commands become available after an integration - # reload, not only after a full Home Assistant restart (previously the - # once-per-instance guard forced a full restart for any newly-added command). - from .ws_api import ( # pylint: disable=import-outside-toplevel - async_load_panel_config, - async_register_commands, - ) - - await async_load_panel_config(hass) # self-guards; safe to call repeatedly - from . import store_account # pylint: disable=import-outside-toplevel - await store_account.async_load(hass) # integration-wide online flag + account - - # Cache the integration version from HA's already-loaded manifest (no file IO). - # ws_get_constants reads it from hass.data; we can't do a module-level read_text - # in ws_api.py because the module is imported lazily inside this coroutine and the - # IO runs on the event loop (#328/#335). - if "ha_washdata_version" not in hass.data: - try: - from homeassistant.loader import async_get_integration as _aget_integration # pylint: disable=import-outside-toplevel - _integ = await _aget_integration(hass, DOMAIN) - hass.data["ha_washdata_version"] = _integ.manifest.get("version", "") or "" - except Exception as err: # pylint: disable=broad-exception-caught - _LOGGER.debug("Could not load ha_washdata version from manifest: %s", err) - # Fall back to reading manifest.json off the event loop. Only - # write the key when the read succeeds so that a double failure - # (loader + file) leaves the key absent rather than storing "" - # (which would shadow _INTEGRATION_VERSION in ws_get_constants). - def _read_manifest_version() -> str: - try: - return json.loads( - (Path(__file__).parent / "manifest.json").read_text(encoding="utf-8") - ).get("version") or "" - except Exception: # pylint: disable=broad-exception-caught - return "" - _fallback_version = await hass.async_add_executor_job(_read_manifest_version) - if _fallback_version: - hass.data["ha_washdata_version"] = _fallback_version - - async_register_commands(hass) - hass.data["ha_washdata_ws_registered"] = True - - # Register conversation intents (e.g. "is my washer done?") - once per HA - # instance. Intents are domain-global, so guard against re-registration when - # more than one device is configured. - if not hass.data.get("ha_washdata_intents_registered"): - from .intents import async_setup_intents # pylint: disable=import-outside-toplevel - - async_setup_intents(hass) - hass.data["ha_washdata_intents_registered"] = True + # Belt and braces for the hoist above: the panel's static routes need + # hass.http, which is an after_dependencies entry rather than a hard one, so + # it is guaranteed up before setup in a real HA but not in every harness. + # By here the entity platforms have pulled the whole frontend stack in, which + # is where this block used to live. Every step guards on its own "already + # done" flag, so this is a no-op whenever the early call did its job. + await _async_setup_shared(hass, _log) # Register feedback service if not hass.services.has_service( @@ -1174,22 +1329,62 @@ def _apply_device_link(hass: HomeAssistant, entry: ConfigEntry) -> None: When CONF_LINKED_DEVICE points at an existing device (e.g. the smart plug or appliance), the WashData device is shown as "Connected via " in the HA device registry. Clearing the option removes the link. Stale targets that - no longer exist are treated as "no link" so the registry never references a - deleted device. + no longer exist - and a target that is this entry's own WashData device, which + HA rejects as a self-reference (#418) - are treated as "no link" so the + registry never references a deleted device or itself. """ + _log = DeviceLoggerAdapter(_LOGGER, entry.title) registry = dr.async_get(hass) - washdata_device = registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) + identifier = (DOMAIN, entry.entry_id) + if hasattr(registry, "async_get_device_by_identifier"): + # HA 2026.9+ deprecated async_get_device because identifiers are no longer + # unique across config entries (issue #405). Our device's identifier is + # owned by this entry, so the by-identifier lookup is unambiguous. The + # attribute guard keeps us working on the older HA the manifest still + # supports, where the new method does not exist yet. + washdata_device = registry.async_get_device_by_identifier( + identifier, entry.entry_id + ) + else: + washdata_device = registry.async_get_device(identifiers={identifier}) if washdata_device is None: return linked_device_id = entry.options.get(CONF_LINKED_DEVICE) or None if linked_device_id and registry.async_get(linked_device_id) is None: linked_device_id = None + if linked_device_id and linked_device_id == washdata_device.id: + # A device may not be its own via_device: HA 2026.9 raises + # HomeAssistantError instead of silently accepting it, and this runs inside + # async_setup_entry - so a self-link aborted setup for the whole entry + # (#418). The picker used to list this entry's own WashData device, whose + # name mirrors the entry title (and often the plug's), so it was easy to + # select by mistake. Treat it as "no link" and fall through to the update + # below: on an older HA the self-reference may already be stored in the + # registry, and clearing it is exactly the repair needed. + _log.warning( + "Ignoring 'Group Under Device': %s is this appliance's own WashData " + "device and a device cannot be linked to itself. Pick the smart plug " + "(or another device) instead, or clear the setting.", + linked_device_id, + ) + linked_device_id = None if washdata_device.via_device_id != linked_device_id: - registry.async_update_device( - washdata_device.id, via_device_id=linked_device_id - ) + try: + registry.async_update_device( + washdata_device.id, via_device_id=linked_device_id + ) + except (HomeAssistantError, ValueError) as err: + # The via_device link is cosmetic (it only nests the device in the HA + # registry UI). A registry rule we do not know about yet must never be + # able to take the whole entry down with it, as the self-link did in + # #418 - log it and leave the device standalone. + _log.warning( + "Could not link WashData device to %s: %s", + linked_device_id or "(none)", + err, + ) async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -1210,6 +1405,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data.get(FORWARDED_ENTRIES_KEY, set()).discard(entry.entry_id) manager = hass.data[DOMAIN].pop(entry.entry_id) await manager.async_shutdown() diff --git a/custom_components/ha_washdata/analysis.py b/custom_components/ha_washdata/analysis.py index 7044eeaa..31120a56 100644 --- a/custom_components/ha_washdata/analysis.py +++ b/custom_components/ha_washdata/analysis.py @@ -34,6 +34,8 @@ from .const import ( MATCH_DTW_REFINE_TOP_N, MATCH_DTW_RESAMPLE_N, MATCH_DURATION_SCALE, + MATCH_DURATION_SCALE_OVERRUN, + MATCH_PREFIX_SHAPE_MAX_RATIO, MATCH_DURATION_WEIGHT, MATCH_ENERGY_SCALE, MATCH_ENERGY_WEIGHT, @@ -390,7 +392,24 @@ def compute_matches_worker( dur_weight = float(config.get("duration_weight", MATCH_DURATION_WEIGHT)) en_weight = float(config.get("energy_weight", MATCH_ENERGY_WEIGHT)) dur_scale = float(config.get("duration_scale", MATCH_DURATION_SCALE)) + dur_overrun_scale = float( + config.get("duration_overrun_scale", MATCH_DURATION_SCALE_OVERRUN) + ) en_scale = float(config.get("energy_scale", MATCH_ENERGY_SCALE)) + # #400: while the cycle is still running, Stage 4 compares like with like. Both + # of its terms describe the cycle SO FAR; without this they are graded against + # each candidate's COMPLETE duration and energy, which makes a long program 40% + # through numerically indistinguishable from a finished short one. Opt-in (live + # match path only) so the final match at cycle end - where the whole-cycle + # figures are the right comparison - is byte-identical. + in_progress = bool(config.get("in_progress")) + # ...and Stages 2/3 score the SHAPE against the same truncated stretch, while the + # cycle is still clearly mid-run (MATCH_PREFIX_SHAPE_MAX_RATIO). On by default + # for a live match; `prefix_shape: False` turns it off for the A/B harnesses. + prefix_shape_on = in_progress and bool(config.get("prefix_shape", True)) + prefix_shape_max_ratio = float( + config.get("prefix_shape_max_ratio", MATCH_PREFIX_SHAPE_MAX_RATIO) + ) curr_arr = np.array(current_power) @@ -405,10 +424,29 @@ def compute_matches_worker( if ratio < min_duration_ratio or ratio > max_duration_ratio: continue - # Core Similarity - score, metrics, offset = find_best_alignment( - current_power, sample_power, 1.0, corr_weight=corr_weight - ) + # Core Similarity. While the cycle is mid-run this compares it against the + # candidate truncated to the elapsed time, on a shared grid (#400) - the + # same pair Stage 3 then warps, so both shape stages ask one question. + # `sample` below stays the FULL template: Stage 4 takes its own prefix of it + # (analysis.prefix_mean), and truncating it here would truncate twice. + span_s = float(item.get("sample_span_s") or profile_duration or 0.0) + shape_pair = None + if ( + prefix_shape_on + and span_s > 0 + and current_duration <= span_s * prefix_shape_max_ratio + ): + shape_pair = prefix_shape_arrays( + curr_arr, sample_power, current_duration, span_s + ) + if shape_pair is not None: + score, metrics, offset = find_best_alignment( + shape_pair[0], shape_pair[1], 1.0, corr_weight=corr_weight + ) + else: + score, metrics, offset = find_best_alignment( + current_power, sample_power, 1.0, corr_weight=corr_weight + ) if score > keep_min: candidates.append({ @@ -418,6 +456,9 @@ def compute_matches_worker( "profile_duration": profile_duration, "current": current_power, "sample": sample_power, + # Transient, popped after Stage 3: the truncated (current, template) + # pair Stage 2 scored, so Stage 3 warps the same thing. + "_shape_pair": shape_pair, # True wall-clock span of `sample`, for prefix truncation (#364). # Falls back to profile_duration so the other snapshot builders # (devtools, matching_tuner, playground) keep working unchanged. @@ -443,10 +484,16 @@ def compute_matches_worker( curr_resampled = _resample_to(curr_arr, MATCH_DTW_RESAMPLE_N) for cand in to_refine: - sample_arr = np.array(cand["sample"]) + pair = cand.get("_shape_pair") + if pair is not None: + warp_curr, sample_arr, cand_resampled = pair[0], pair[1], None + else: + warp_curr, sample_arr, cand_resampled = ( + curr_arr, np.array(cand["sample"]), curr_resampled + ) dtw_score, norm_dist = _stage3_dtw_score( - curr_arr, + warp_curr, sample_arr, current_peak, dtw_mode=dtw_mode, @@ -454,7 +501,7 @@ def compute_matches_worker( l1_scale=l1_scale, ddtw_scale=ddtw_scale, ensemble_w=ensemble_w, - curr_resampled=curr_resampled, + curr_resampled=cand_resampled, ) cand["original_score"] = float(cand["score"]) @@ -463,6 +510,11 @@ def compute_matches_worker( candidates.sort(key=lambda x: x["score"], reverse=True) + # The truncated pair is scratch for the two shape stages; it must not reach the + # MatchResult ranking (numpy arrays, and the store/WS serialise that dict). + for cand in candidates: + cand.pop("_shape_pair", None) + # Final pass: blend in duration + energy agreement. Shape correlation alone # cannot separate profiles that differ mainly in duration/energy (the main # multi-program washing-machine failure mode), so nudge the score toward @@ -488,10 +540,29 @@ def compute_matches_worker( cur_energy = cur_mean * current_duration if integrated else cur_mean for cand in candidates: prof_dur = float(cand.get("profile_duration") or 0.0) - dur_ag = _agreement(current_duration, prof_dur, dur_scale) + if in_progress and prof_dur > 0 and current_duration > prof_dur: + # The cycle has outlasted this candidate: real evidence against it, + # penalised on the sharper scale. Below a candidate's duration the + # term is unchanged - suppressing the penalty there was measured and + # rejected (see MATCH_DURATION_SCALE_OVERRUN in const.py). + dur_ag = _agreement(current_duration, prof_dur, dur_overrun_scale) + else: + dur_ag = _agreement(current_duration, prof_dur, dur_scale) sample = cand.get("sample") or [] - cand_mean = float(np.mean(sample)) if sample else 0.0 - cand_energy = cand_mean * prof_dur if integrated else cand_mean + if in_progress: + cand_mean, cand_span = prefix_mean( + sample, + current_duration, + float(cand.get("sample_span_s") or prof_dur or 0.0), + prof_dur, + ) + else: + cand_mean = float(np.mean(sample)) if sample else 0.0 + cand_span = prof_dur + # In integrated mode the candidate figure must cover the same stretch of + # time as cur_energy does, so a prefix mean is scaled by the elapsed + # duration and a whole-template mean by the candidate's own duration. + cand_energy = cand_mean * cand_span if integrated else cand_mean en_ag = _agreement(cur_energy, cand_energy, en_scale) cand["shape_score"] = float(cand["score"]) cand["score"] = float( @@ -509,6 +580,35 @@ def compute_matches_worker( return candidates +def prefix_mean( + sample: list[float] | np.ndarray, + current_duration: float, + sample_span_s: float, + profile_duration: float, +) -> tuple[float, float]: + """Mean power of ``sample`` over its leading ``current_duration`` seconds, and + the span that mean covers - the Stage-4 like-for-like pair for a cycle that is + still running (#400). + + The Stage-4 like-for-like pair, and the same pair Stage 5 uses to choose between + a group's members while a cycle is running - one definition, two callers. + + Falls back to the whole template (and the candidate's own duration) when the + cycle has already outlasted it: that candidate has finished, so its total is + the honest comparison. Deliberately NOT ``_prefix_point_count``: that helper's + 12-sample floor exists because Stage 6 *correlates* the prefix, while a mean + over a handful of leading samples is perfectly well defined - applying the + floor here would silently restore whole-template energy for the first few + percent of every cycle, which is exactly the window #400 is about. + """ + if len(sample) == 0: + return 0.0, profile_duration + if sample_span_s > 0 and current_duration < sample_span_s: + k = max(1, int(round(len(sample) * (current_duration / sample_span_s)))) + return float(np.mean(sample[:k])), current_duration + return float(np.mean(sample)), profile_duration + + def _prefix_point_count( n_points: int, current_duration: float, sample_span_s: float ) -> int: @@ -528,6 +628,47 @@ def _prefix_point_count( return k +def prefix_shape_arrays( + curr_arr: np.ndarray, + sample: list[float] | np.ndarray, + current_duration: float, + sample_span_s: float, +) -> tuple[np.ndarray, np.ndarray] | None: + """``(current, template)`` on one grid, with the template TRUNCATED to the + elapsed time - or None when it cannot be truncated meaningfully. + + One definition of "the same stretch of both curves", shared by the two callers + that need it: the live Stage-2/3 shape scoring (#400) and the Stage-6 prefix + guard (#364). Both series go onto a shared grid so an index offset equals a time + offset regardless of the template's native cadence; the grid also honours the + #388 OOM cap. The 12-sample floor is real here (unlike in ``prefix_mean``): + these arrays get correlated and warped, not averaged. + + The grid is shared **between the two series**, not across candidates: ``k`` + is the candidate's own truncated point count, so a longer template can be + scored on a finer grid than a shorter one. That asymmetry is deliberate and + measured. Capping the grid at ``k`` is what stops ``arr[:k]`` being upsampled + past the points it actually has, which would invent template detail the + recording never contained. Dropping the ``k`` term to make the grid purely + candidate-independent (``min(curr_arr.size, MAX_ALIGN_GRID_POINTS)``) was + tried and measured on ``devtools/prefix_guard_eval.py``: at the shipped + constants it takes the #364 split guard from **59/114 caught (52%) to 52/114 + (46%)** while removing only 2 of 13 false blocks. A miss there is a SPLIT + CYCLE and a false block is merely a later finish, so that trade is + net-negative. ``devtools/dtw_ab_eval.py`` is byte-identical either way + (it scores only complete cycles, which never take the prefix path), so it + cannot be used to judge this function - use ``prefix_guard_eval.py``. + """ + arr = np.asarray(sample, dtype=float) + k = _prefix_point_count(arr.size, current_duration, sample_span_s) + if k == 0: + return None + grid = int(min(curr_arr.size, k, MAX_ALIGN_GRID_POINTS)) + if grid < SMART_TERM_PREFIX_MIN_POINTS: + return None + return _resample_to(curr_arr, grid), _resample_to(arr[:k], grid) + + def prefix_shape_score( curr_arr: np.ndarray, sample: list[float] | np.ndarray, @@ -554,18 +695,10 @@ def prefix_shape_score( default is ``"ensemble"`` and the live ProfileStore path never sets ``dtw_mode``; ``"legacy"`` exists only for the devtools re-sweep harness. """ - arr = np.asarray(sample, dtype=float) - k = _prefix_point_count(arr.size, current_duration, sample_span_s) - if k == 0: + pair = prefix_shape_arrays(curr_arr, sample, current_duration, sample_span_s) + if pair is None: return None - prefix = arr[:k] - # Put both series on one grid so index offset equals time offset regardless of - # the template's native cadence, and honour the #388 OOM cap. - grid = int(min(curr_arr.size, k, MAX_ALIGN_GRID_POINTS)) - if grid < SMART_TERM_PREFIX_MIN_POINTS: - return None - a = _resample_to(curr_arr, grid) - b = _resample_to(prefix, grid) + a, b = pair corr_weight = float(config.get("corr_weight", MATCH_CORR_WEIGHT)) score, _metrics, _offset = find_best_alignment(a, b, 1.0, corr_weight=corr_weight) diff --git a/custom_components/ha_washdata/const.py b/custom_components/ha_washdata/const.py index 0e8ec007..19687aee 100644 --- a/custom_components/ha_washdata/const.py +++ b/custom_components/ha_washdata/const.py @@ -138,6 +138,8 @@ CONF_ANTI_WRINKLE_EXIT_POWER = "anti_wrinkle_exit_power" # W threshold for true CONF_ANTI_WRINKLE_IDLE_TIMEOUT = "anti_wrinkle_idle_timeout" # Seconds below exit power before anti-wrinkle ends CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE = "dishwasher_end_spike_quiet_release" # Dishwasher: sustained-quiet seconds after expected duration that release the end-of-cycle drain wait early (#379) CONF_SMART_TERMINATION_DURATION_RATIO = "smart_termination_duration_ratio" # Fraction of the matched profile's expected (mean) duration that Smart Termination requires before it may fire (#393) +CONF_ANTI_CREASE_FINALIZE_RATIO = "anti_crease_finalize_ratio" # Fraction of the matched profile's expected (mean) duration the anti-crease finalise requires before it may fire (#429) +CONF_CURVE_PREROLL_SECONDS = "curve_preroll_seconds" # How far back readings from aborted start probes may be carried into a committed cycle's curve; 0 = off (#430) CONF_DELAY_START_DETECT_ENABLED = "delay_start_detect_enabled" # Enable delayed-start detection CONF_DELAY_CONFIRM_SECONDS = "delay_confirm_seconds" # Seconds power must stay in standby band before DELAY_WAIT engages CONF_DELAY_TIMEOUT_HOURS = "delay_timeout_hours" # Safety timeout (hours) while waiting to start @@ -163,12 +165,37 @@ CONF_NOTIFY_LIVE_CHRONOMETER = "notify_live_chronometer" # (sticky) and a tap target (clickAction). Not new notification types. Mobile-only. CONF_NOTIFY_LIVE_STICKY = "notify_live_sticky" CONF_NOTIFY_LIVE_CLICK_ACTION = "notify_live_click_action" +# Silent recurring live updates (#417). iOS alerts on every Live Activity refresh +# unless the update is marked silent, so a 10-minute live interval buzzes the phone +# all cycle long. Mobile-only, and never applied to the update that STARTS the +# activity - that one stays audible (and `silent` has no effect there anyway). +CONF_NOTIFY_LIVE_SILENT = "notify_live_silent" CONF_NOTIFY_REMINDER_MESSAGE = "notify_reminder_message" # Distinct one-time pre-end alert CONF_NOTIFY_TIMEOUT_SECONDS = "notify_timeout_seconds" # Auto-dismiss after N seconds (0 = never) CONF_NOTIFY_CHANNEL = "notify_channel" # Android channel for status/live/reminder CONF_NOTIFY_FINISH_CHANNEL = "notify_finish_channel" # Distinct Android channel for finished/clean CONF_ENERGY_PRICE_STATIC = "energy_price_static" CONF_ENERGY_PRICE_ENTITY = "energy_price_entity" +# Dynamic (time-weighted) pricing (#426). With a price *entity* configured, the +# cost of a cycle is integrated against the price in force at each moment instead +# of freezing the single price that happened to be current when the cycle ended. +# Only meaningful for a price entity - a static price has no time dimension, so +# this is a no-op there and the flag is never consulted. Default on: the frozen +# end-of-cycle price is simply wrong for a tariff that moves during the cycle, +# and cost is display-only (no detection, matching or ML input depends on it). +CONF_ENERGY_PRICE_DYNAMIC = "energy_price_dynamic" +DEFAULT_ENERGY_PRICE_DYNAMIC = True +# Cap on the stored per-cycle price timeline. Entries are deduplicated (a price +# that did not change adds nothing), so an hourly tariff needs a handful and this +# only bites on a template sensor that recomputes every few seconds. Past the cap +# the timeline is coarsened by dropping the smallest price steps, which keeps the +# cost figure within a rounding error of the uncapped one while bounding what a +# cycle adds to the JSON store. +PRICE_TIMELINE_MAX_POINTS = 240 +# Prices are rounded to this many decimals before dedup. Currency-per-kWh figures +# are quoted to 4-5 decimals at most; the extra digit keeps sub-cent tariffs exact +# while collapsing the float noise a template sensor emits on every recompute. +PRICE_TIMELINE_PRICE_DECIMALS = 6 # Optional external cumulative energy meter (issue #316). When set, each cycle's # reported energy is taken from this counter's start->end delta instead of the # integrated power trace, which systematically under-counts on report-on-change @@ -230,6 +257,10 @@ DEFAULT_NOTIFY_LIVE_OVERRUN_PERCENT = 20 DEFAULT_NOTIFY_LIVE_CHRONOMETER = False DEFAULT_NOTIFY_LIVE_STICKY = False # #347: off = today's behaviour (tap dismisses) DEFAULT_NOTIFY_LIVE_CLICK_ACTION = "" # #347: empty = no tap target (today's behaviour) +# #417: on by default. A progress refresh is not an alert, and every other app with +# live progress updates silently; the audible per-update buzz was the complaint, not +# the feature. Turn it off to get a sound/vibration on every update again. +DEFAULT_NOTIFY_LIVE_SILENT = True DEFAULT_NOTIFY_TIMEOUT_SECONDS = 0 # 0 = notifications never auto-dismiss DEFAULT_NOTIFY_CHANNEL = "" # Empty = omit channel (companion app default) DEFAULT_NOTIFY_FINISH_CHANNEL = "" # Empty = reuse status channel @@ -303,6 +334,59 @@ DEFAULT_WATCHDOG_INTERVAL = 30 # Floor; effective default is resolved per devic DEFAULT_MATCH_PERSISTENCE = 3 DEFAULT_END_REPEAT_COUNT = 1 # 1 = current behavior (no repeat required) +# Share of the SHORTEST known profile that the match-interval suggestion is +# allowed to spend before a program can first be committed (#431). The +# suggestion used to be cadence-only (`median_dt * 10`), so a plug reporting +# every 60 s produced 599 s - longer than DEFAULT_PROFILE_MATCH_INTERVAL itself, +# which makes applying the suggestion strictly worse than never touching the +# setting. The budget is spent on `match_persistence` consecutive matches, so +# the cap is applied to `interval * persistence` rather than to the interval +# alone; otherwise raising persistence brings the problem straight back. At the +# default persistence of 3 this is exactly the "shortest / 20" rule the reporter +# proposed (3 / 20 = 0.15). It bounds only the *suggestion* - a hand-set +# interval is still whatever the user typed. +MATCH_INTERVAL_SUGGESTION_DECISION_FRAC = 0.15 + +# Absolute floor for the same suggestion, and the one case where the budget rule +# above does NOT hold: a very short program (below ~200 s at the default +# persistence) would cap the interval into a per-second poll, so the floor wins +# and the decision budget then exceeds the fraction. The suggestion says so +# rather than claiming a bound it did not apply. Matching is driven by incoming +# readings, so a 10 s interval on a 60 s-reporting plug still only matches per +# reading; the floor costs nothing there. +MATCH_INTERVAL_SUGGESTION_MIN_S = 10 + +# Issue #430: a cycle's curve begins at the start probe that finally COMMITS. +# Earlier probes that aborted as false starts take their readings with them, so +# on an appliance that probes repeatedly before settling (programme selection, +# door lock, first fill) the first 40-217 s of real activity - up to 17 readings +# on the reporter's dishwashers - is missing from the front of every curve. #403 +# makes this more common, not less: the first high reading now earns no evidence +# toward either start gate, so a sparse change-only sensor aborts more probes. +# +# The detector keeps its own small ring buffer rather than reaching into the +# manager's diag_buffer: that one is manager-owned and records RAW readings +# before throttling, while the curve is built from throttled ones, so joining +# the two would splice two different sample populations into one curve. +# +# OFF BY DEFAULT, and it must stay that way. The stored duration is +# ``end_time - _current_cycle_start`` and matching resamples the stored curve, so +# the start pointer has to move back with the curve or the two disagree - which +# means avg_duration shifts. Cycles recorded before and after a change therefore +# carry different durations for the SAME program, widening the envelope and +# moving expected_duration (which arms Smart Termination x0.98 and the #429 +# anti-crease ratio) until the old cycles age out of the retention cap. +DEFAULT_CURVE_PREROLL_SECONDS = 0.0 # 0 = off +# Upper bound on the option (the panel's number input offers the same maximum), so +# a mistyped value cannot drag minutes of unrelated standby into a curve. It is +# headroom, not a measurement: the probe runs above were 40-217 s. +CURVE_PREROLL_MAX_SECONDS = 600.0 +# A quiet stretch longer than this ends the carry: it separates "the same start, +# probed twice" from "an unrelated blip earlier in the day". Deliberately a +# constant, not an option - it is a property of how appliances probe, and one +# more knob here is one more way to widen a curve by accident. +PREROLL_CHAIN_BREAK_SECONDS = 90.0 + # Matching & Termination Stability DEFAULT_MATCH_REVERT_RATIO = 0.4 # Drop from peak score to revert to detecting DEFAULT_DEFER_FINISH_CONFIDENCE = 0.55 # Minimum confidence to defer cycle finish @@ -348,6 +432,17 @@ CYCLE_OVERRUN_ANOMALY_RATIO = 1.5 ENDING_HARD_FINALIZE_RATIO = 2.0 ENDING_HARD_FINALIZE_MIN_QUIET_S = 600.0 # continuous sub-threshold span floor +# Cap on how far the RUNNING->PAUSED / PAUSED->ENDING gates may be stretched by +# the p95 sampling cadence (#424/#427). The gates are 3x a cadence estimate; p95 +# is the 2nd-largest of the last 20 intervals, so a publish-on-change plug that +# falls silent at standby drives the estimate onto its own silence and the gates +# grow with it. ``CycleDetector._gate_cadence`` therefore clamps p95 to this +# multiple of the median interval. 5x is deliberately loose: it is above any +# plausible jitter ratio for a regularly-reporting sensor (whose median equals +# its p95, so the cap never binds and slow meters keep their wide gates) while +# still rejecting the isolated multi-minute holes that caused both reports. +GATE_CADENCE_MEDIAN_FACTOR = 5.0 + # NOTE: STANDBY_BAND_* constants live further down, after the DEVICE_TYPE_* # definitions they reference (search "Standby-band stuck-in-RUNNING finalize"). # Underrun anomaly: a cycle that finishes in less than this fraction of its @@ -468,6 +563,12 @@ MATCH_MAE_SCALE = 100.0 # half-saturation point of the MAE score curv MATCH_MAE_REF_PEAK = 1000.0 # peak (W) at which scoring matches the legacy formula MATCH_MAE_PEAK_FLOOR = 50.0 # floor so tiny/idle traces don't explode the ratio MATCH_KEEP_MIN_SCORE = 0.1 # candidates scoring below this are discarded +# Shortest resampled current-cycle trace the matcher will score. Below this a +# correlation is noise, so both match paths decline rather than return a number +# nobody should act on. Named because the value was duplicated as a bare literal +# in async_match_profile and in the Playground's matcher, and the two drifted: +# the sim scored 5-point stretches that production had already rejected. +MATCH_MIN_RESAMPLED_POINTS = 12 # DTW refinement (Stage 3): blended = DTW_BLEND*core + (1-DTW_BLEND)*dtw_score, # dtw_score = DIST_SCALE / (DIST_SCALE + scaled_dtw_distance). MATCH_DTW_BLEND = 0.5 @@ -594,6 +695,30 @@ MATCH_DURATION_WEIGHT = 0.22 MATCH_ENERGY_WEIGHT = 0.22 MATCH_DURATION_SCALE = 0.175 # ~ln ratio at which duration agreement halves MATCH_ENERGY_SCALE = 0.25 # ~ln ratio at which energy agreement halves +# Issue #400: once a RUNNING cycle has outlasted a candidate, that is hard +# evidence against it, and the penalty uses this sharper scale instead of +# MATCH_DURATION_SCALE. Only reached when the caller opts in via +# config["in_progress"]; the final match at cycle end keeps the symmetric term, +# where elapsed IS the cycle's true duration. +# +# Below a candidate's duration the term is deliberately UNCHANGED. Suppressing the +# penalty there ("we simply have not got there yet") was measured and rejected: it +# adds only +0.7pp mid-cycle top-1 over prefix energy alone, costs 3.7pp at the 90% +# checkpoint, and - because it hands a longer sibling full duration agreement near +# the short one's end - it puts a dishwasher's 50 deg and 65 deg programmes inside +# MATCH_AMBIGUITY_MARGIN of each other at the end of the 50 deg, which reads as +# ambiguous and blocks Smart Termination (measured on four real exports; #393 is +# about finishing on time, so that is not a trade worth 0.7pp). +MATCH_DURATION_SCALE_OVERRUN = 0.05 +# Issue #400, shape half: while a cycle is running, Stages 2 and 3 score it against +# each candidate TRUNCATED to the elapsed time (reusing the #364 prefix machinery), +# but only while it is still clearly mid-run. Past this fraction of a candidate's +# own span the truncation starts discarding the very thing that separates a short +# programme from its longer sibling at the end - "I have already run longer than +# everything you have shown me". Measured on cycle_data: mid-cycle top-1 62.6% -> +# 71.0% at 0.7; at 0.8 the 90% checkpoint drops and a real dishwasher export loses +# Smart Termination, the same cliff the rejected duration credit fell off. +MATCH_PREFIX_SHAPE_MAX_RATIO = 0.7 # States @@ -741,9 +866,69 @@ STANDBY_BAND_FLATNESS_FLOOR_W = 2.0 # absolute flatness floor for low-peak dev # that never heats is left alone. Asymmetric (finalise-only, can only shorten the # wait) and self-correcting (a new wash's heating burst leaves the regime and # re-arms matching). -ANTI_CREASE_FINALIZE_RATIO = 0.98 # elapsed must reach 98% of expected duration +# Issue #429: the ratio is per-appliance (CONF_ANTI_CREASE_FINALIZE_RATIO, range +# 0.50-1.00, empty = this default), for the same reason as #393 - and note that +# this is a DIFFERENT gate from CONF_SMART_TERMINATION_DURATION_RATIO, which +# gates Smart Termination rather than the finalise into STATE_ANTI_WRINKLE. +# ``_expected_duration`` is the profile's outlier-filtered arithmetic MEAN, so on +# a sensor-dry program (runtime follows the load, not the clock) a fixed 98% of +# the mean is unreachable for about half the runs by construction, and the path +# built for exactly that tail can never engage. Measured by the reporter on a +# condenser dryer: 15 of 15 matched runs landed at 0.38-0.92 of expected and all +# 15 ended by fallback timeout, 14-39 min (median 27) after the last real +# activity; at 0.75 the finalise fired on all 3 subsequent runs, 6-12 min after. +# +# Left per-appliance rather than re-defaulted, and NOT device-type-resolved, +# because the safe value is a property of the individual machine: the reporter's +# heat-pump dryer has no comparable tumble tail and closes 0.8-1.5 min after its +# last high reading, so the fixed ratio costs it nothing. +# +# LOWER IT ON DRYERS, NOT ON WASHING MACHINES. On a washer this ratio is the +# whole discriminator between the post-wash tail and a mid-wash trough (see the +# rationale above): a washer spends most of its cycle below +# ``anti_wrinkle_max_power``, so the low-power window check cannot tell the two +# apart, and neither can the #364 trailing-power check (``_smart_term_power_ +# plausible`` compares against the profile's OWN tail level, which is also low). +# ``_anticrease_spin_pending`` (#399) covers the terminal-spin case but fails +# open when the profile carries no terminal high block. +DEFAULT_ANTI_CREASE_FINALIZE_RATIO = 0.98 # elapsed must reach 98% of expected duration +# The range the panel's number input enforces (min 0.5 / max 1.0), restated here so +# the detector can hold a stored value to it. Nothing else validates the range: +# `import_config` strips only nulls, a selective import writes numbers through, and +# the Playground sanitizer casts to float. A stored 0.0 would make the past-expected +# test `current_duration < expected * 0.0` pass for every non-negative duration, i.e. +# remove the discriminator this gate rests on for washing machines (see above). +ANTI_CREASE_FINALIZE_RATIO_MIN = 0.5 +ANTI_CREASE_FINALIZE_RATIO_MAX = 1.0 +# Backwards-compatible alias: the pre-#429 module-level constant. Kept so older +# imports (and anything pinned in the lab) still resolve; the detector reads the +# per-device config field, never this. +ANTI_CREASE_FINALIZE_RATIO = DEFAULT_ANTI_CREASE_FINALIZE_RATIO ANTI_CREASE_CONFIRM_WINDOW_S = 180.0 # recent window that must hold no reading > max_power +# Issue #399: both conditions above look BACKWARDS, so a wash whose final spin +# lands just past 0.98 x expected - preceded by more than the confirm window below +# `anti_wrinkle_max_power` (a delicate/rinse stretch) - was finalised seconds +# before its own spin, and the spin then opened a second cycle record. The guard +# asks the matched profile whether it ends with a high-power block and, if so, +# refuses to finalise until THIS run has produced its counterpart. +# +# Deliberately event-based, not clock-based: blocking merely until elapsed passes +# the profile's own last high sample delays the reported finalise by 16 s and then +# splits the wash anyway, because a run's spin can sit hundreds of seconds later +# than the profile's (load-dependent duration). Asymmetric like the other +# anti-crease guards - it can only ever DELAY a finalise - and bounded by the cap +# below so a program that legitimately skips its spin can never hang. +ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC = 0.90 # profile's last high block must START this + # late in its run to count as terminal; + # a genuinely low-power tail (the #296 + # Miele tumble) never arms the guard +ANTI_CREASE_TERMINAL_MATCH_FRAC = 0.5 # live high-power seconds after that + # position, as a fraction of the + # profile's own block, that count as + # "this run has had its spin" +ANTI_CREASE_SPIN_WAIT_MAX_RATIO = 1.25 # never block past this x expected + # Device Type Defaults # Device Type Defaults (Maps) @@ -866,6 +1051,22 @@ DEFAULT_OFF_DELAY_BY_DEVICE = { DEVICE_TYPE_PUMP: 20, # 20 s (Pumps cut off sharply; no warm-down phase) } +# Ceiling for the manager's *unmatched* zombie guard (seconds), i.e. the failsafe +# that force-ends a cycle with no learned expected duration (expected == 0). This is +# only a last-resort kill for a stuck FALSE START; the detector already hard-caps any +# cycle at 8h (28800s), and the guard additionally only fires when the appliance is +# effectively idle and no external end trigger is available (issue #404). All values +# MUST stay below the detector's 28800s cap so the guard remains an *earlier* kill. +# Wet/long appliances get a longer fuse because a genuine wash+dry or long cottons +# programme with no matched profile can legitimately run well past 4h. +DEFAULT_UNMATCHED_WATCHDOG_CEILING = 14400 # 4h scalar fallback +DEFAULT_UNMATCHED_WATCHDOG_CEILING_BY_DEVICE = { + DEVICE_TYPE_WASHING_MACHINE: 21600, # 6h (long cottons + pre-wash) + DEVICE_TYPE_WASHER_DRYER: 25200, # 7h (combined wash+dry runs 6+h) + DEVICE_TYPE_DRYER: 21600, # 6h (anti-crease can extend a long dry) + DEVICE_TYPE_DISHWASHER: 18000, # 5h (long eco + silent drying pauses) +} + # Device-specific progress smoothing thresholds (percentage points) # These control how much backward progress is allowed before heavy damping kicks in DEVICE_SMOOTHING_THRESHOLDS = { @@ -1004,6 +1205,20 @@ def resolve_smart_termination_duration_ratio_default(device_type: str) -> float: # distinct programs <~0.6; 0.80 leaves margin below the 0.85 suggestion bar. GROUP_MIN_COHESION = 0.80 +# Per-profile terminal signature (`profile_store.compute_profile_terminal_signature`). +# Measured over every export in `cycle_data/` (146 unique dishwasher cycles, 10 +# households): a dishwasher's terminal pump-out peaks at a median 1.3% of its own +# cycle peak (p10 0.8%, p90 3.8%) and follows a median 906 s of quiet. Both bounds +# are therefore RELATIVE - a fixed wattage does not survive the range, which is +# exactly why the #399 spin extractor (fixed at `anti_wrinkle_max_power`, 400 W) +# finds no terminal block at all on a dishwasher whose pump-out is 33 W. +# The quiet minimum separates "the event after the drying phase" from an ordinary +# gap inside the wash; 120 s is well under the measured p10 of 624 s. +TERMINAL_EVENT_PEAK_FRAC = 0.004 +TERMINAL_QUIET_MIN_S = 120.0 +# Below this many evidence cycles the medians describe noise, not the programme. +TERMINAL_SIGNATURE_MIN_CYCLES = 3 + # Storage # v6: backfill ml_review.golden=True for manually-recorded cycles (recorded == # golden reference; a single flag, no duplicate "recorded" field). diff --git a/custom_components/ha_washdata/cycle_detector.py b/custom_components/ha_washdata/cycle_detector.py index 6db05789..ca06f58b 100644 --- a/custom_components/ha_washdata/cycle_detector.py +++ b/custom_components/ha_washdata/cycle_detector.py @@ -22,7 +22,7 @@ import itertools import logging import math from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from typing import Any, Callable, cast import numpy as np @@ -65,14 +65,23 @@ from .const import ( TERMINAL_DROP_OFF_DELAY_SECONDS, ENDING_HARD_FINALIZE_RATIO, ENDING_HARD_FINALIZE_MIN_QUIET_S, + GATE_CADENCE_MEDIAN_FACTOR, STANDBY_BAND_FINALIZE_DEVICE_TYPES, STANDBY_BAND_MIN_RATIO, STANDBY_BAND_WINDOW_S, STANDBY_BAND_MAX_FRACTION, STANDBY_BAND_FLATNESS_FRACTION, STANDBY_BAND_FLATNESS_FLOOR_W, - ANTI_CREASE_FINALIZE_RATIO, + DEFAULT_ANTI_CREASE_FINALIZE_RATIO, + ANTI_CREASE_FINALIZE_RATIO_MIN, + ANTI_CREASE_FINALIZE_RATIO_MAX, + DEFAULT_CURVE_PREROLL_SECONDS, + CURVE_PREROLL_MAX_SECONDS, + PREROLL_CHAIN_BREAK_SECONDS, ANTI_CREASE_CONFIRM_WINDOW_S, + ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC, + ANTI_CREASE_TERMINAL_MATCH_FRAC, + ANTI_CREASE_SPIN_WAIT_MAX_RATIO, ) # The dishwasher end-spike wait window is shared between two code paths @@ -117,6 +126,47 @@ _LOGGER = logging.getLogger(__name__) STOP_LOCKOUT_RELEASE_SECONDS = 180.0 +def effective_anticrease_finalize_ratio(value: Any) -> float: + """The ratio the anti-crease gate will actually use for a stored value. + + Only ``ws_set_options`` range-checks this option: ``import_config`` strips + nulls only, a selective import writes numbers through, and the Playground + sanitizer just casts to float. So the value is held to its documented range + here, at every point of use, and anything unusable falls back to the default + rather than disarming the gate (a stored ``0.0`` would satisfy the + past-expected test for every duration). + + Shared with the Playground's config summary so the figure the panel shows and + the figure the gate applies cannot drift apart. + """ + try: + ratio = float(value) + except (TypeError, ValueError, OverflowError): + return DEFAULT_ANTI_CREASE_FINALIZE_RATIO + if not math.isfinite(ratio): + return DEFAULT_ANTI_CREASE_FINALIZE_RATIO + return min( + ANTI_CREASE_FINALIZE_RATIO_MAX, max(ANTI_CREASE_FINALIZE_RATIO_MIN, ratio) + ) + + +def effective_curve_preroll_seconds(value: Any) -> float: + """The pre-roll window actually applied for a stored value (0 = off). + + Same reasoning as :func:`effective_anticrease_finalize_ratio`: the detector + caps the window at ``CURVE_PREROLL_MAX_SECONDS`` wherever it reads it, so the + summary has to report the capped figure or it describes a sim that did not + run. + """ + try: + window = float(value or 0.0) + except (TypeError, ValueError, OverflowError): + return 0.0 + if not math.isfinite(window) or window <= 0: + return 0.0 + return min(window, CURVE_PREROLL_MAX_SECONDS) + + @dataclass class CycleDetectorConfig: """Configuration for cycle detection.""" @@ -166,6 +216,16 @@ class CycleDetectorConfig: # on. The dishwasher pump-out relief is combined via min(), so a configured value # can only loosen the gate. smart_termination_duration_ratio: float = DEFAULT_SMART_TERMINATION_DURATION_RATIO + # Fraction of the matched profile's expected duration the anti-crease finalise + # requires before it may fire (#429). A DIFFERENT gate from the ratio above: + # that one gates Smart Termination, this one the finalise into + # STATE_ANTI_WRINKLE. Scalar default (no device-type resolution) and always a + # real float, so playground.effective_settings() never sees None. + anti_crease_finalize_ratio: float = DEFAULT_ANTI_CREASE_FINALIZE_RATIO + # How far back readings from aborted start probes may be carried into a + # committed cycle's curve (#430). 0 disables the whole path, which is the + # default and keeps the stored-duration convention unchanged. + curve_preroll_seconds: float = DEFAULT_CURVE_PREROLL_SECONDS delay_detect_enabled: bool = False # Sustained seconds power must stay in the standby band (between # stop_threshold_w and start_threshold_w) before DELAY_WAIT engages. @@ -293,8 +353,18 @@ class CycleDetector: # Data self._power_readings: list[tuple[datetime, float]] = [] # (time, raw_power) + # Rolling pre-cycle readings, so a start that took several probes to + # commit can recover the readings the aborted probes took with them + # (#430). Only appended to while no cycle is open, trimmed to + # curve_preroll_seconds, and cleared at every cycle end - a previous + # cycle's tail must never be carried into the next cycle's curve. + self._preroll_buffer: list[tuple[datetime, float]] = [] self._current_cycle_start: datetime | None = None self._last_active_time: datetime | None = None + # Last reading the POWER SENSOR actually sent, as opposed to one the + # manager injected to advance the quiet timers. Deliberately not reset per + # cycle: it describes the sensor, not the run. + self._last_real_reading_time: datetime | None = None self._cycle_max_power: float = 0.0 # Accumulators (dt-aware) @@ -322,6 +392,12 @@ class CycleDetector: # Adaptive Sampling Tracker self._recent_dts: list[float] = [] # Track last 20 dt values self._p95_dt: float = 1.0 # Default assumption + # The cadence as it stood BEFORE the reading currently being processed was + # folded in. Every gap-vs-outage classification reads this, never _p95_dt: + # an outage-sized interval that has already widened p95 would raise the very + # ceiling meant to catch it (a 120 s gap after a 10 s cadence lifts p95 to + # ~15.5 s -> ceiling 155 s -> the gap passes as observed time). + self._prior_p95_dt: float = 1.0 # Profile Matching Tracker self._last_match_time: datetime | None = None @@ -339,6 +415,17 @@ class CycleDetector: # Mean power the matched profile draws over the last few % of its own run # (profile_store.profile_tail_power). None = no opinion, guard stays inert. self._matched_tail_power: float | None = None + # (start_frac, seconds, start_offset_s) of the matched profile's own terminal + # high-power block, from profile_store.profile_terminal_high_block. None = the + # profile has no such block (or we have no opinion) and the guard stays inert + # (#399). A two-element payload (pre-item-196 snapshot, Playground, older + # callers) is still accepted and falls back to start_frac x expected. + self._matched_terminal_high: ( + tuple[float, float] | tuple[float, float, float] | None + ) = None + # One-shot per cycle, so the held-finalise reason is visible in the log + # without repeating it on every reading. + self._anticrease_spin_wait_logged: bool = False self._last_smart_term_block_reason: str | None = None # #346 diagnostic throttle # Anti-wrinkle tracking (dryers only) @@ -382,18 +469,48 @@ class CycleDetector: # turning off. self._preserve_delay_band_on_off: bool = False + @property + def _gate_cadence(self) -> float: + """Cadence the pause/end gates are sized from (#424/#427). + + ``_p95_dt`` is the 2nd-largest of the last 20 intervals by construction, + so it tracks the worst *gap* rather than the reporting rate. That is the + right input for the outage ceilings (a gap must be judged against the + worst gap we consider normal) but the wrong one for the gates below, + which multiply it by three: once a publish-on-change plug falls silent at + standby, the only intervals left are the long quiet ones, p95 collapses + onto them, and each gate becomes ~3x the silence it is supposed to be + measuring. The accumulator advances one interval per reading, so the + cycle then needs ~3 more readings - a gate that is set by, and grows + with, its own input. Measured on the #427 trace: a 297 s sensor silence + followed by a 481 s keepalive lifted the end gate from 45 s to 1455 s and + held the cycle in PAUSED for 25.5 min. + + Capping p95 at a multiple of the *median* keeps the estimate robust: a + genuinely slow sensor reports slowly every time, so its median equals its + p95 and the cap never binds (a 300 s-cadence meter keeps its 900 s gate); + a fast sensor that went quiet has a small median, so the isolated holes + cannot triple the gate. Only these two gates read it - ``_p95_dt`` itself + is left alone so every outage ceiling keeps the cadence snapshot it was + tuned against (register items 213, 215). + """ + if len(self._recent_dts) < 5: + return self._p95_dt + median_dt = float(np.median(self._recent_dts)) + return min(self._p95_dt, GATE_CADENCE_MEDIAN_FACTOR * median_dt) + @property def _dynamic_pause_threshold(self) -> float: """Calculate dynamic pause threshold based on sampling cadence.""" # User requirement: T_pause >= 3 * p95_update_interval # Default 15s or 3 * p95 - return max(15.0, 3.0 * self._p95_dt) + return max(15.0, 3.0 * self._gate_cadence) @property def _dynamic_end_threshold(self) -> float: """Calculate dynamic end candidate threshold.""" # Keep this generic for pause->ending transitions across all device types. - base = 3.0 * self._p95_dt + base = 3.0 * self._gate_cadence # Ensure end threshold is at least 15s greater than pause threshold return max(base, self._dynamic_pause_threshold + 15.0) @@ -533,12 +650,74 @@ class CycleDetector: return None try: value = float(raw) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): + # OverflowError alongside the type errors: `json` keeps an integer + # literal of any length as an unbounded `int`, and `float()` on one + # raises rather than returning `inf`, so the non-finite filter below is + # never reached. Both callers are the reason it matters - the restored + # state snapshot is hand-editable `.storage` JSON, and + # `restore_state_snapshot`'s one broad `except` answers a raise with + # `self.reset()`, which discards the WHOLE restored cycle rather than + # this one field. "No opinion" is the documented contract; a full reset + # is not. return None if not math.isfinite(value) or value <= 0: return None return value + @staticmethod + def _sanitize_terminal_high( + raw: Any, + ) -> tuple[float, float] | tuple[float, float, float] | None: + """Coerce ``raw`` into a ``(start_frac, seconds)`` pair or a + ``(start_frac, seconds, start_offset_s)`` triple, else None (#399). + + None means "no opinion", which leaves ``_anticrease_spin_pending`` inert and + the anti-crease finalise exactly as it behaved before the guard existed. + + The arity is PRESERVED rather than normalised (register item 196). A + two-element payload is what a pre-196 state snapshot, an older caller and + most tests supply, and it has to keep meaning "no absolute offset, fall back + to ``start_frac x expected``" - handing it a fabricated 0.0 offset would make + the guard scan the whole cycle. A malformed third element degrades to the + pair for the same reason the whole method returns None on garbage: it must + never be able to disarm a guard that would otherwise arm. + """ + if raw is None: + return None + # A str/bytes is iterable, so `list("11")` is `["1", "1"]` and sanitizes to + # (1.0, 1.0) - a scalar string silently ARMING the guard off a malformed + # snapshot, which is the one direction this method promises never to go. + # Rejected before the iteration so it takes the documented garbage path. + if isinstance(raw, (str, bytes, bytearray)): + return None + try: + values = list(raw) + except TypeError: + return None + if len(values) not in (2, 3): + return None + try: + start_frac = float(values[0]) + seconds = float(values[1]) + except (TypeError, ValueError, OverflowError): + # Same reason as _sanitize_tail_power above: an unbounded int raises out + # of float(), and a raise here costs the whole restored cycle state. + return None + if not math.isfinite(start_frac) or not math.isfinite(seconds): + return None + if not 0.0 <= start_frac <= 1.0 or seconds <= 0: + return None + if len(values) == 2: + return (start_frac, seconds) + try: + start_offset = float(values[2]) + except (TypeError, ValueError, OverflowError): + return (start_frac, seconds) + if not math.isfinite(start_offset) or start_offset < 0: + return (start_frac, seconds) + return (start_frac, seconds, start_offset) + def _trailing_mean_power(self, timestamp: datetime, window_s: float) -> float | None: """Time-weighted mean power over the trailing ``window_s``, or None when there are too few samples to judge. @@ -565,7 +744,7 @@ class CycleDetector: # = clip(10x cadence, 60, 3600)), NOT _outage_threshold_s() which rebuilds a # NumPy array from every reading - this runs on the per-reading ENDING / # anti-crease path, same reasoning as the gap-free tally at L1006. - max_gap = min(3600.0, max(60.0, 10.0 * self._p95_dt)) + max_gap = min(3600.0, max(60.0, 10.0 * self._prior_p95_dt)) cut = 0 for i in range(1, len(window)): if (window[i][0] - window[i - 1][0]).total_seconds() > max_gap: @@ -732,6 +911,13 @@ class CycleDetector: self._matched_tail_power = ( self._sanitize_tail_power(result_seq[8]) if len(result_seq) >= 9 else None ) + # Element 10 (#399): the matched profile's own terminal high-power + # block. Cleared by a shorter tuple for the same reason as element 9 - + # keeping the previous profile's block would make the anti-crease guard + # wait for a spin the newly-matched program does not have. + self._matched_terminal_high = ( + self._sanitize_terminal_high(result_seq[9]) if len(result_seq) >= 10 else None + ) else: # Assume MatchResult object or similar (future proofing) # But for now wrapper returns tuple @@ -744,6 +930,7 @@ class CycleDetector: self._match_prefix_ambiguous = False self._match_prefix_ambiguous_full_shape = False self._matched_tail_power = None + self._matched_terminal_high = None elif match_name: # If sanitization rejected the expected_duration, treat the match @@ -775,6 +962,10 @@ class CycleDetector: """Force reset the detector state to target state.""" self._transition_to(target_state, dt_util.now()) self._power_readings = [] + # #430: the pre-roll buffer is pre-CYCLE context, never cross-cycle. A + # reset that left it populated would let the previous cycle's tail be + # spliced into the front of the next cycle's curve. + self._preroll_buffer = [] self._current_cycle_start = None self._last_active_time = None self._cycle_max_power = 0.0 @@ -797,6 +988,8 @@ class CycleDetector: self._match_prefix_ambiguous = False self._match_prefix_ambiguous_full_shape = False self._matched_tail_power = None + self._matched_terminal_high = None + self._anticrease_spin_wait_logged = False # Per-cycle diagnostic throttle (#346): the "Smart Termination not applied" # line only logs when the reason CHANGES. Carrying the previous cycle's # reason across a reset swallows the new cycle's very first diagnostic @@ -925,8 +1118,26 @@ class CycleDetector: return min(configured_ratio, 0.90) return configured_ratio - def process_reading(self, power: float, timestamp: datetime) -> None: - """Process a new power reading using robust dt-aware logic.""" + def process_reading( + self, power: float, timestamp: datetime, synthetic: bool = False + ) -> None: + """Process a new power reading using robust dt-aware logic. + + ``synthetic=True`` marks a reading the *manager* injected rather than one + the power sensor sent: the watchdog and anti-wrinkle keepalives, which + exist to advance the quiet timers while a change-only plug says nothing. + They must keep doing exactly that, so this flag changes no timing here. + It is recorded only so that anything reasoning about what was OBSERVED can + tell the two apart. **Nothing consumes it yet**: it was added for + `_keep_tail_cap` (register item 238) and that use was implemented, + measured and reverted, because after `_last_active_time` every reading is + below the stop threshold anyway, so a plug still reporting cannot separate + a drying phase from standby - it only shows the plug is chatty. Kept + because the distinction is correct and cheap to carry; see item 260 for + the two other consumers that were measured and rejected. + """ + if not synthetic: + self._last_real_reading_time = timestamp # Calculate dt (needed by the stop lockout below and the state machine). dt = 0.0 @@ -956,7 +1167,15 @@ class CycleDetector: self._lockout_high_seconds += dt if self._lockout_high_seconds < STOP_LOCKOUT_RELEASE_SECONDS: # Still within the spin-down window - ignore reading. + # The reading is withheld from the state machine, but it is + # still a real observation of the power level, so record it + # (#403): the accumulator below judges each interval against + # the previous observation, and a release reading compared to + # a pre-stop sample up to the full lockout window old would + # lose the credit for its own interval (#267 back-to-back + # start whose stop happened in a low-power trough). self._last_process_time = timestamp + self._last_power = power return self._ignore_power_until_idle = False self._lockout_high_seconds = 0.0 @@ -973,10 +1192,15 @@ class CycleDetector: # outage that has already widened p95 would raise the very threshold that # is supposed to catch it (a 120 s gap after a 10 s cadence lifts p95 to # ~15.5 s -> ceiling 155 s -> the gap counts as observed quiet). - prior_p95_dt = self._p95_dt + self._prior_p95_dt = self._p95_dt self._update_cadence(dt) self._last_process_time = timestamp + # 1b. Pre-roll buffer (#430): record every reading seen while no cycle is + # open, so a start that needed several probes can recover what the aborted + # ones took with them. Cheap and bounded; inert while the option is off. + self._record_preroll(power, timestamp) + # 1. Smoothing (Legacy buffer for debug/display, logic uses raw + time accumulators) self._ma_buffer.append(power) if len(self._ma_buffer) > self._config.smoothing_window: @@ -991,18 +1215,54 @@ class CycleDetector: is_high = power >= threshold + # Last observation carried forward (#403): `dt` is the interval that + # ENDED at this reading, so the appliance sat at the PREVIOUS sample's + # level for it, not at this one. With a change-only (send-on-delta) + # power sensor a low -> high crossing carries the whole idle gap, and + # crediting it at the new high power let a single blip after minutes of + # silence satisfy both start gates on the next reading. So the interval + # only counts as high-power evidence when the previous observation was + # also at or above the threshold those gates measure against. A densely + # sampled device is unaffected: there the previous sample is already + # high and the interval keeps its full credit. + # + # This is the same principle the surrounding code already applies - the + # low branch restarts its gap-free tally rather than credit an outage, + # DELAY_WAIT and the paused-STARTING anchor (#306) anchor on the first + # high reading, and `integrate_wh`/`energy_gap_threshold_s` drop + # outage-sized segments - applied to the one branch that still credited + # unobserved time. An outage heuristic cannot substitute for it: a + # 511 s gap on a 70 s idle cadence is legitimate change-only silence, + # well inside the outage ceiling, and only the credit direction + # separates it from real high-power time. + # + # A reading inside the hysteresis band (>= stop_threshold_w but + # < start_threshold_w) therefore earns no evidence toward the start + # gates, which is correct: the band is by definition below the + # threshold the gates measure against, and it is exactly where a + # waiting machine idles. The cost is one extra report before + # confirmation on a band-crossing ramp; no start is lost. + prev_high = self._last_power is not None and self._last_power >= threshold + high_dt = dt if prev_high else 0.0 + # ...and the ENERGY for that interval at the level the appliance actually sat + # at, which is the same argument applied to the second start gate. Crediting + # it at the NEW reading's power let a sample barely above the threshold, + # followed by a spike, bank the spike's power for the whole preceding + # interval and satisfy start_energy_threshold on its own. Computed here, not + # at the three use sites, because `self._last_power` is overwritten a few + # lines below - before the two STARTING seeds further down would read it. + # The sibling paths already do this: the DELAY_WAIT seed credits at + # `start_power` and the anti-wrinkle window uses the trapezoid average. + high_step_wh = ( + (self._last_power or 0.0) * (high_dt / 3600.0) if high_dt > 0 else 0.0 + ) + if is_high: - self._time_above_threshold += dt + self._time_above_threshold += high_dt self._time_below_threshold = 0.0 self._time_below_threshold_gapfree = 0.0 - # Energy integration (trapezoidal approx for this single step) - # prev_p = self._last_power if self._last_power is not None else power - # step_wh = ((power + prev_p) / 2.0) * (dt / 3600.0) - # Simplified: just P * dt for short steps is fine, - # or call integrate_wh on buffer if needed. - # Let's use simple rect/trapz here for running sum - step_wh = power * (dt / 3600.0) - self._energy_since_idle_wh += step_wh + # Energy for the guarded interval, computed with high_dt above. + self._energy_since_idle_wh += high_step_wh self._last_active_time = timestamp else: self._time_below_threshold += dt @@ -1012,7 +1272,7 @@ class CycleDetector: # 3600)) but reuses the maintained p95 cadence to stay O(1) in this # per-reading hot path. Uses the cadence as it stood BEFORE this # reading, so a gap cannot widen its own acceptance threshold. - outage_ceiling = min(3600.0, max(60.0, 10.0 * prior_p95_dt)) + outage_ceiling = min(3600.0, max(60.0, 10.0 * self._prior_p95_dt)) if dt > outage_ceiling: self._time_below_threshold_gapfree = 0.0 else: @@ -1082,7 +1342,9 @@ class CycleDetector: self._energy_since_idle_wh = max(0.0, avg_power * (interval_s / 3600.0)) else: self._power_readings = [(timestamp, power)] - self._energy_since_idle_wh = power * (dt / 3600.0) if dt > 0 else 0.0 + # Guarded interval (#403): the gap between anti-wrinkle + # tumbles was spent at the previous (idle) level. + self._energy_since_idle_wh = high_step_wh self._cycle_max_power = max(candidate_peak, power) elif self._state != STATE_ANTI_WRINKLE: @@ -1193,8 +1455,14 @@ class CycleDetector: self._transition_to(STATE_STARTING, timestamp) self._current_cycle_start = timestamp self._power_readings = [(timestamp, power)] - self._energy_since_idle_wh = power * (dt / 3600.0) if dt > 0 else 0.0 + # Seed from the guarded interval (#403), not raw dt: this seed + # OVERWRITES the accumulator (it has to - entering STARTING from + # a terminal state carries the previous cycle's total), so an + # unguarded seed would reinstate the idle gap the accumulator + # just declined to credit. + self._energy_since_idle_wh = high_step_wh self._cycle_max_power = power + self._apply_curve_preroll(timestamp, power) # NOTE: terminal-state expiry (Finished/Interrupted/Force-Stopped -> Off) # is owned solely by the manager (WashDataManager._handle_state_expiry), # which has a wall-clock timer that also fires when a change-only power @@ -1244,6 +1512,13 @@ class CycleDetector: if timestamp != start_timestamp: self._power_readings.append((timestamp, power)) self._cycle_max_power = max(start_power, power) + # #430: the buffer records in DELAY_WAIT too, so the + # readings between the anchor and this confirmation exist + # and would otherwise be dropped - the curve would span the + # confirmation window with two points. Never moves the + # anchor forward (see _apply_curve_preroll), and is a no-op + # while the option is off, which is the default. + self._apply_curve_preroll(timestamp, power) else: # Power dropped back below start threshold - clear the # high-power streak anchor so the next high reading @@ -1731,12 +2006,15 @@ class CycleDetector: getattr(self, "_last_match_confidence", 0.0), end_spike_seen, ) - # Keep tail when smart terminating (matches profile duration) + # Keep tail when smart terminating (matches profile + # duration), but only as far as the program can + # actually reach - see _keep_tail_cap (#424). self._finish_cycle( timestamp, status="completed", termination_reason=TerminationReason.SMART, keep_tail=True, + tail_cap=self._keep_tail_cap(start_time), ) return @@ -1879,7 +2157,12 @@ class CycleDetector: # to _last_active_time which may be set by a terminal drain # spike mid-ENDING, producing a falsely short cycle duration. keep_tail = self._config.device_type == "dishwasher" - self._finish_cycle(timestamp, status="completed", keep_tail=keep_tail) + self._finish_cycle( + timestamp, + status="completed", + keep_tail=keep_tail, + tail_cap=self._keep_tail_cap(start_time), + ) return # Compute energy in recent window @@ -1896,7 +2179,12 @@ class CycleDetector: return keep_tail = self._config.device_type == "dishwasher" - self._finish_cycle(timestamp, status="completed", keep_tail=keep_tail) + self._finish_cycle( + timestamp, + status="completed", + keep_tail=keep_tail, + tail_cap=self._keep_tail_cap(start_time), + ) else: self._logger.debug( @@ -1905,6 +2193,139 @@ class CycleDetector: self._config.end_energy_threshold, ) + def _record_preroll(self, power: float, timestamp: datetime) -> None: + """Buffer a reading seen before a cycle commits (#430). + + Only while no cycle is open - once RUNNING, ``_power_readings`` is the + curve and this buffer would just duplicate it. STARTING counts as "not + open": a probe in STARTING may still abort, and those are precisely the + readings worth keeping. + + Bounded by ``curve_preroll_seconds`` (itself capped at + ``CURVE_PREROLL_MAX_SECONDS``), so the buffer holds seconds of data, not + an unbounded history. + """ + window = effective_curve_preroll_seconds(self._config.curve_preroll_seconds) + if window <= 0: + # Option off: keep the buffer empty rather than paying to fill one + # nothing will read, and so that enabling it mid-run cannot splice in + # readings from before the option was turned on. + if self._preroll_buffer: + self._preroll_buffer = [] + return + if self._state not in ( + STATE_OFF, + STATE_STARTING, + STATE_DELAY_WAIT, + STATE_UNKNOWN, + ): + return + self._preroll_buffer.append((timestamp, float(power))) + cutoff = timestamp - timedelta(seconds=window) + # Readings arrive in order, so the stale prefix is contiguous. + drop = 0 + for ts, _p in self._preroll_buffer: + if ts < cutoff: + drop += 1 + else: + break + if drop: + del self._preroll_buffer[:drop] + + def _preroll_for_commit( + self, timestamp: datetime, power: float + ) -> list[tuple[datetime, float]]: + """Readings to prepend to a cycle committing at ``timestamp`` (#430). + + Walks the buffer backwards from the commit and stops at the first quiet + gap longer than ``PREROLL_CHAIN_BREAK_SECONDS`` - "the same start, probed + twice" rather than "an unrelated blip earlier". Then anchors on the + EARLIEST reading in that chain that is at or above ``start_threshold_w``: + anchoring on the window edge instead would drag standby into the curve + and move the cycle start to a moment the appliance was not yet doing + anything. + + Returns [] whenever there is nothing to add, so the caller's fast path is + a single emptiness test. + """ + window = effective_curve_preroll_seconds(self._config.curve_preroll_seconds) + if window <= 0 or not self._preroll_buffer: + return [] + + # Everything strictly before this commit, most recent first. + prior = [(ts, p) for ts, p in self._preroll_buffer if ts < timestamp] + if not prior: + return [] + + chain: list[tuple[datetime, float]] = [] + next_ts = timestamp + for ts, p in reversed(prior): + if (next_ts - ts).total_seconds() > PREROLL_CHAIN_BREAK_SECONDS: + break + chain.append((ts, p)) + next_ts = ts + if not chain: + return [] + chain.reverse() # chronological + + threshold = float(self._config.start_threshold_w) + anchor = next( + (i for i, (_ts, p) in enumerate(chain) if p >= threshold), None + ) + if anchor is None: + return [] # the chain is all standby - nothing of this cycle in it + return chain[anchor:] + + def _apply_curve_preroll(self, timestamp: datetime, power: float) -> None: + """Prepend buffered pre-commit readings to the freshly-started cycle (#430). + + Moves ``_current_cycle_start`` back with them, and that is not optional: + the stored duration is ``end_time - _current_cycle_start`` while matching + resamples ``_power_readings``, so a curve that started earlier than the + pointer would describe a different run from the one whose duration is + recorded. The two existing back-anchors (the anti-wrinkle candidate + window and the DELAY_WAIT high-start anchor) move the pointer for exactly + the same reason. + + **Record-only: this must never make a cycle easier to START.** + ``_energy_since_idle_wh`` is deliberately left alone. Despite the name it + is not the cycle's energy - the stored figure is integrated from + ``power_data`` at persistence, so it picks the pre-roll up for free - it + is the accumulator the STARTING -> RUNNING gate reads + (``>= start_energy_threshold``). Feeding it the pre-roll would let an + aborted probe's energy be re-spent on the next probe's start gate, so two + blips that each failed the gate could together pass it: exactly the + phantom cycle #403 was fixed to prevent. The same argument covers + ``_time_above_threshold``, which is likewise untouched. + + ``_cycle_max_power`` IS updated, because that is a property of the run + being recorded (it gates the anti-crease path), not of admitting it. + """ + preroll = self._preroll_for_commit(timestamp, power) + if not preroll: + return + + start_ts = preroll[0][0] + # Callers do not all commit with the pointer at ``timestamp``. The + # DELAY_WAIT confirmation has already back-anchored it to its first + # sustained-high reading, and a pre-roll window shorter than + # ``start_duration_threshold`` would otherwise move that pointer FORWARD + # and shorten the delayed start. So keep whatever the caller anchored + # before the chain, and only ever move the pointer earlier. + earlier = [(ts, p) for ts, p in self._power_readings if ts < start_ts] + self._power_readings = [*earlier, *preroll, (timestamp, power)] + self._current_cycle_start = min( + start_ts, self._current_cycle_start or start_ts + ) + self._cycle_max_power = max(p for _ts, p in self._power_readings) + self._logger.debug( + "Curve pre-roll: carried %d reading(s) covering %.0fs from aborted " + "start probe(s) into this cycle (start moved back to %s).", + len(preroll), + (timestamp - start_ts).total_seconds(), + start_ts.isoformat(), + ) + def _transition_to(self, new_state: str, timestamp: datetime) -> None: """Handle state transitions.""" if self._state == new_state: @@ -2133,6 +2554,18 @@ class CycleDetector: separates the post-wash anti-crease tail from a mid-wash low-power trough (a washer spends most of its cycle below ``anti_wrinkle_max_power``, but a mid-wash trough is always BEFORE the expected duration, the tail after it). + + That "past expected" fraction is per-appliance since #429 + (``anti_crease_finalize_ratio``, default 0.98). **Lowering it trades away + exactly the guarantee in the paragraph above**, so it is meant for dryers + whose sensor-dry runtime follows the load and whose tumble tail would + otherwise sit until the fallback timeout. Nothing downstream can stand in + for it on a washer: the low-power window check cannot separate a trough + from a tail (both are below ``anti_wrinkle_max_power`` by definition), + ``_smart_term_power_plausible`` compares the trailing mean against the + matched profile's OWN tail level, which is equally low, and + ``_anticrease_spin_pending`` fails open when the profile carries no + terminal high block. """ if not self._config.anti_wrinkle_enabled: return False @@ -2162,7 +2595,15 @@ class CycleDetector: if start is None: return False current_duration = (timestamp - start).total_seconds() - if current_duration < self._expected_duration * ANTI_CREASE_FINALIZE_RATIO: + # Held to the documented 0.50-1.00 range on READ, not at construction: the + # manager assigns this field directly on an options reload, and the value can + # arrive from an import or the Playground, neither of which range-checks it. + # A stored 0.0 would satisfy the test below for every duration and hand the + # gate a mid-wash trough. + finalize_ratio = effective_anticrease_finalize_ratio( + self._config.anti_crease_finalize_ratio + ) + if current_duration < self._expected_duration * finalize_ratio: return False # #364: "past expected" only means "past the wash" when expected belongs to # the RIGHT profile. A whole washer wash phase sits below @@ -2215,6 +2656,12 @@ class CycleDetector: """ if not self._anticrease_gate_open(timestamp): return False + # #399: only the finalise, never _anticrease_gate_open. A false block in the + # shared gate would also kill the match freeze, and because the tumble bursts + # recur faster than off_delay neither the fallback timeout nor + # ENDING_HARD_FINALIZE could then close the cycle - that is the #296 hang. + if self._anticrease_spin_pending(timestamp): + return False max_power = float(self._config.anti_wrinkle_max_power) # Walk the tail; readings are chronological so we can break once outside the # window (O(window), not O(n)). @@ -2254,6 +2701,132 @@ class CycleDetector: return False # a heating / high-spin reading in the window - still washing return True + def _anticrease_spin_pending(self, timestamp: datetime) -> bool: + """Whether the matched profile still owes this run a terminal high-power + event - i.e. the anti-crease finalise must wait (#399). + + ``_is_anticrease_tail``'s two conditions both look backwards: past expected, + and quiet for the confirm window. A programme whose final spin lands just + past 0.98 x expected, after a long sub-``anti_wrinkle_max_power`` rinse + stretch, satisfies both while the spin is still ahead - so the wash was + finalised into anti-wrinkle and the spin opened a SECOND cycle record. + + The profile carries the missing information: where its own last high-power + block sits and how long it runs. If that block is terminal (starts at or + after ``ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC`` of the profile) and this run + has not yet produced a comparable amount of high-power time at or after the + same position, the spin is still ahead. + + Deliberately compares EVENTS, not clock positions: mapping the profile's + last high sample onto elapsed time and clearing there delays the reported + finalise by 16 s and then splits the wash anyway, because a run's spin can + arrive hundreds of seconds later than the profile's (the same + load-dependent duration spread behind #393). + + Delay-only and bounded: never blocks past + ``ANTI_CREASE_SPIN_WAIT_MAX_RATIO`` x expected, and fails open on any + missing input, so it cannot reproduce the #296 hang. + """ + block = self._matched_terminal_high + if block is None: + return False + start_frac, block_seconds = block[0], block[1] + if start_frac < ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC: + return False # the profile's tail is genuinely low-power (#296 shape) + expected = self._expected_duration + start = self._current_cycle_start + if expected <= 0 or start is None: + return False + current_duration = (timestamp - start).total_seconds() + if current_duration >= expected * ANTI_CREASE_SPIN_WAIT_MAX_RATIO: + return False # cap: waited long enough, let the finalise through + needed = block_seconds * ANTI_CREASE_TERMINAL_MATCH_FRAC + if needed <= 0: + return False + # Register item 196: scan from the block's ABSOLUTE offset on the profile's + # own grid when the store supplied one (element 3). `start_frac` is measured + # against the quiet-TRIMMED span - it has to be, or a capture's idle tail + # disarms the terminal gate above - while `expected` is the profile's + # avg_duration, which tracks the UNTRIMMED span. Their product is therefore a + # systematically LATE offset, and a late offset means the run's own spin sits + # BEFORE the scan window and is never counted, so the hold ran out the + # ANTI_CREASE_SPIN_WAIT_MAX_RATIO cap instead of releasing on the event. Over + # 36 real armed profile/cycle pairs the product recognised the spin 3 times + # and the absolute offset 14, with zero cases in either where the credited + # seconds exceeded the run's own terminal block (so no new premature-release + # exposure). The fallback keeps a pre-196 payload - an old state snapshot, the + # Playground, older callers - behaving exactly as before. + offset_s = float(block[2]) if len(block) >= 3 else start_frac * expected + seen = self._high_power_seconds_since(offset_s) + if seen >= needed: + return False + if not self._anticrease_spin_wait_logged: + self._anticrease_spin_wait_logged = True + self._logger.debug( + "Anti-crease finalize held: '%s' ends with a %.0fs block above %.0fW " + "at %.0f%% of its run (scanning from %.0fs); this cycle has %.0fs of " + "it so far (elapsed %.0fs of %.0fs expected).", + self._matched_profile, + block_seconds, + float(self._config.anti_wrinkle_max_power), + start_frac * 100.0, + offset_s, + seen, + current_duration, + expected, + ) + return True + + def _high_power_seconds_since(self, offset_s: float) -> float: + """Seconds this cycle has spent above ``anti_wrinkle_max_power`` at or after + ``offset_s`` from its start (#399). + + Walks the readings backwards and stops at the offset, so the scan is bounded + by the tail of the trace rather than its whole length. Each reading covers + the interval up to the following one, which matches how the profile's own + block length is measured. + + Two corrections to that per-interval credit, both of which decide whether the + guard releases: + + * An outage-sized interval is unobserved time, not high-power time. Counting + it in full let a silent plug bank minutes of "spin" it never reported, + satisfy ``seen >= needed`` and release the finalise before the real + terminal spin - the #399 failure, reached by a different route. Same + treatment (and the same p95-derived ceiling) the tail scan at + ``_smart_term_tail_stats`` and the gap-free quiet tally already apply. + Deliberately NOT ``_outage_threshold_s()``, which rebuilds a NumPy array + from every reading; this runs on the per-reading anti-crease path. + * When ``offset_s`` falls inside an interval, only the part after the offset + counts. Breaking out of the loop dropped that remainder entirely, and the + offset is ``start_frac * expected``, so a boundary reading is the norm + rather than an edge case. + """ + start = self._current_cycle_start + if start is None or not self._power_readings: + return 0.0 + ceiling = float(self._config.anti_wrinkle_max_power) + max_gap = min(3600.0, max(60.0, 10.0 * self._prior_p95_dt)) + total = 0.0 + readings = self._power_readings + for i in range(len(readings) - 1, -1, -1): + ts, power = readings[i] + elapsed = (ts - start).total_seconds() + if i + 1 >= len(readings): + continue # last reading covers no interval yet + next_elapsed = (readings[i + 1][0] - start).total_seconds() + if next_elapsed <= offset_s: + break # this interval ends at or before the offset, as do all earlier ones + interval = next_elapsed - elapsed + if interval > max_gap: + if elapsed < offset_s: + break + continue # unobserved time, not evidence of anything + if float(power) > ceiling: + # Credit only the portion at or after the offset. + total += next_elapsed - max(elapsed, offset_s) + return total + def _maybe_finalize_anticrease_tail(self, timestamp: datetime) -> bool: """Finalise a cycle that has entered the anti-crease tail into STATE_ANTI_WRINKLE (#296). Returns True if the cycle was finalised. @@ -2280,6 +2853,24 @@ class CycleDetector: status="completed", termination_reason=TerminationReason.SMART, keep_tail=True, + # Capped like the three sibling keep_tail finishes (#424). This path can + # fire on a window of *watchdog* keepalives: a change-only plug that has + # gone silent emits nothing, the injected 0 W readings satisfy the "all + # at or below anti_wrinkle_max_power" window, and `timestamp` is then + # the moment the watchdog noticed rather than the moment the appliance + # stopped - post-cycle standby banked as cycle time, which feeds + # avg_duration and self-amplifies. + # + # The tumble tail itself is never cut into, which is why this is safe + # here: an anti-crease baseline sits ABOVE stop_threshold + # (const.py:811-812, a ~2.5-3.2 W draw against a ~1.2 W threshold), so + # every one of those readings refreshes `_last_active_time` and the cap + # - max(expected_end, _last_active_time) - lands at the last tumble. A + # real tail therefore loses only the trailing quiet gap between its last + # reading and this finalize, which is time the appliance drew nothing. + # A tail of genuinely-0 W readings is clipped back to the matched + # profile's expected end. Shorten-only, never earlier than expected_end. + tail_cap=self._keep_tail_cap(start_time), ) return True @@ -2496,12 +3087,47 @@ class CycleDetector: # Tertiary check: If duration exceeded max tolerance, allow finish (failsafe). return False + def _keep_tail_cap(self, start_time: datetime) -> datetime | None: + """Latest end time a *kept* tail may claim (#424). + + The paths that keep their tail do so because the tail can be real cycle + time: a dishwasher's near-0 W passive drying phase sits between the last + drain spike and the actual end of the programme (issue #43), so snapping + back to ``_last_active_time`` would store a falsely short cycle. But they + fire on accumulated quiet time, and on a publish-on-change plug that wait + is minutes of *post-appliance* standby - which stamping ``timestamp`` as + the end time banked as cycle time. + + Measured on the #424 reporter's dishwasher: every cycle that ended via + `timeout` stored a 0-29 s tail (237-239 min, matching the appliance), and + every cycle that ended via `smart` stored a 96-1239 s tail (240-260 min), + with ``_last_active_time`` unchanged across the whole history - only the + termination path differed. It also self-amplifies, because the inflated + duration feeds ``avg_duration``, which raises ``expected_duration``, + which delays the next Smart Termination further (the second reporter's + profile had already drifted 63 -> 70.5 min). + + So cap the kept tail at whichever is later: the last above-threshold + reading, or the matched profile's expected end. Time after *both* is time + the appliance drew nothing AND that lies beyond the known length of the + programme it matched, so nothing real can live there. Asymmetric - + shorten-only, and never earlier than the expected end, so a genuine + passive drying phase still lands inside the stored cycle. Returns None + for an unmatched cycle, which has no expected end to anchor against and + is left exactly as before. + """ + if self._expected_duration <= 0: + return None + expected_end = start_time + timedelta(seconds=self._expected_duration) + return max(expected_end, self._last_active_time or expected_end) + def _finish_cycle( self, timestamp: datetime, status: str = "completed", termination_reason: str = TerminationReason.TIMEOUT, keep_tail: bool = False, + tail_cap: datetime | None = None, ) -> None: """Finalize cycle. @@ -2513,11 +3139,19 @@ class CycleDetector: trailing zero readings (e.g. Smart Termination). If False (default), snap back to last active time and trim trailing zeros (e.g. Timeout). + tail_cap: Latest end time a kept tail may claim. Ignored when + ``keep_tail`` is False or when it is not earlier than + ``timestamp``; readings past it are dropped so the stored + trace and the stored duration stay consistent. """ # Capture data before reset + readings = self._power_readings if keep_tail: end_time = timestamp + if tail_cap is not None and tail_cap < end_time: + end_time = tail_cap + readings = [r for r in readings if r[0] <= end_time] else: end_time = self._last_active_time or timestamp @@ -2536,7 +3170,7 @@ class CycleDetector: # Trim leading/trailing zero readings for cleaner data # If we keep tail, we explicitly do NOT trim end zeros trimmed_readings = trim_zero_readings( - self._power_readings, + readings, threshold=self._config.stop_threshold_w, trim_end=not keep_tail, ) @@ -2652,6 +3286,7 @@ class CycleDetector: "match_prefix_ambiguous": self._match_prefix_ambiguous, "match_prefix_ambiguous_full_shape": self._match_prefix_ambiguous_full_shape, "matched_tail_power": self._matched_tail_power, + "matched_terminal_high": self._matched_terminal_high, "ml_defer_start_duration": self._ml_defer_start_duration, } @@ -2722,6 +3357,9 @@ class CycleDetector: self._matched_tail_power = self._sanitize_tail_power( snapshot.get("matched_tail_power") ) + self._matched_terminal_high = self._sanitize_terminal_high( + snapshot.get("matched_terminal_high") + ) self._ml_defer_start_duration = snapshot.get("ml_defer_start_duration") # Restore state enter time and recompute time_in_state from it diff --git a/custom_components/ha_washdata/diagnostics.py b/custom_components/ha_washdata/diagnostics.py index 5cbc6ce6..f8fe8dc4 100644 --- a/custom_components/ha_washdata/diagnostics.py +++ b/custom_components/ha_washdata/diagnostics.py @@ -23,7 +23,7 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import CONF_AUTO_MAINTENANCE, DEFAULT_AUTO_MAINTENANCE, DOMAIN from .manager import WashDataManager # Keys that can identify the user or their home network - redacted in all contexts. @@ -101,10 +101,33 @@ async def async_get_config_entry_diagnostics( else {} ), "profile_sample_repair_stats": manager.profile_sample_repair_stats, + # Programs the matcher cannot even consider (no evidence cycle behind them), + # which used to be a debug log only - #400 was reported against a device in + # exactly this state, where the right program could neither win a match nor + # veto a shorter look-alike. + "unmatchable_profiles": manager.profile_store.unmatchable_profiles(), "suggestions": manager.profile_store.get_suggestions(), + # Read each flag off the object that actually owns it. Two of these used + # to be read off the manager, which owns neither, so they reported False + # for everyone and sent triage down the wrong path (issue #407). "feature_flags": { - "auto_maintenance": bool(getattr(manager, "_auto_maintenance", False)), - "save_debug_traces": bool(getattr(manager, "_save_debug_traces", False)), + # No runtime mirror on the manager: the option is read on demand in + # _setup_maintenance_scheduler. Report the effective option *and* + # whether the midnight job actually armed, so a divergence points at + # the scheduler instead of looking like a stale flag. + "auto_maintenance": bool( + entry.options.get( + CONF_AUTO_MAINTENANCE, + entry.data.get(CONF_AUTO_MAINTENANCE, DEFAULT_AUTO_MAINTENANCE), + ) + ), + "auto_maintenance_scheduled": ( + getattr(manager, "_remove_maintenance_scheduler", None) is not None + ), + # Lives on the store, not the manager. + "save_debug_traces": bool( + getattr(manager.profile_store, "save_debug_traces", False) + ), "notify_fire_events": bool(getattr(manager, "_notify_fire_events", False)), }, }, diff --git a/custom_components/ha_washdata/frontend.py b/custom_components/ha_washdata/frontend.py index cb98d4d0..0d0476e3 100644 --- a/custom_components/ha_washdata/frontend.py +++ b/custom_components/ha_washdata/frontend.py @@ -20,7 +20,7 @@ import asyncio import logging import os from pathlib import Path -from typing import Any, Literal, TypedDict, cast +from typing import Any, Literal, NamedTuple, TypedDict, cast from homeassistant.core import HomeAssistant, Event from homeassistant.const import EVENT_COMPONENT_LOADED @@ -66,6 +66,45 @@ CARD_DEFERRED = "deferred" CARD_FAILED = "failed" CardRegisterResult = Literal["registered", "deferred", "failed"] +# How the card ended up loaded (or why it did not), carried out of +# _init_resource so the caller can log the truth instead of a guess (#432). +# ``extra_module`` is a SUCCESS that creates no Lovelace resource at all: with +# YAML-managed resources the card is injected via frontend.add_extra_js_url, so +# the old blanket "Auto-registered lovelace resource" line named the wrong +# mechanism - and only at debug, which is why #384 needed debug logging enabled +# before anyone could see which path had run. +RESOURCE_CREATED = "resource_created" +RESOURCE_UPDATED = "resource_updated" +RESOURCE_CURRENT = "resource_current" +RESOURCE_EXTRA_MODULE = "extra_module" +RESOURCE_HELPERS_UNAVAILABLE = "lovelace_helpers_unavailable" +RESOURCE_LOVELACE_UNAVAILABLE = "lovelace_storage_unavailable" + +# Human-readable reason per failure mode, used in the warnings so a user can act +# on the log line without turning on debug. +_RESOURCE_FAILURE_REASONS = { + RESOURCE_HELPERS_UNAVAILABLE: ( + "Lovelace resource helpers are unavailable in this Home Assistant build" + ), + RESOURCE_LOVELACE_UNAVAILABLE: "Lovelace storage is not available", +} + + +class ResourceInitResult(NamedTuple): + """Outcome of one Lovelace resource initialisation attempt. + + ``ok`` keeps the old boolean contract for the callers' control flow; ``mode`` + is one of the ``RESOURCE_*`` constants above and exists purely so the log + line can name the path that actually ran. + """ + + ok: bool + mode: str + + def describe(self) -> str: + """Reason text for a warning, falling back to the raw mode.""" + return _RESOURCE_FAILURE_REASONS.get(self.mode, self.mode) + class LovelaceResourceItem(TypedDict, total=False): """Known lovelace resource item shape used by this integration.""" @@ -306,8 +345,15 @@ def _register_static_path(hass: HomeAssistant, url_path: str, path: str) -> bool return False -async def _init_resource(hass: HomeAssistant, url: str, ver: str) -> bool: - """Safely add or update a Lovelace resource for the given URL.""" +async def _init_resource( + hass: HomeAssistant, url: str, ver: str +) -> ResourceInitResult: + """Safely add or update a Lovelace resource for the given URL. + + Returns ``ResourceInitResult(ok, mode)``; ``mode`` names which of the two + load mechanisms ran (a real Lovelace resource, or the ``add_extra_js_url`` + fallback used when resources are YAML-managed) or why neither could (#432). + """ try: # pylint: disable=import-outside-toplevel from homeassistant.components.frontend import add_extra_js_url @@ -318,12 +364,12 @@ async def _init_resource(hass: HomeAssistant, url: str, ver: str) -> bool: _LOGGER.debug( "Lovelace resource helpers unavailable; skipping auto resource init" ) - return False + return ResourceInitResult(False, RESOURCE_HELPERS_UNAVAILABLE) lovelace = hass.data.get("lovelace") if not lovelace: _LOGGER.debug("Lovelace storage not available; skipping auto resource init") - return False + return ResourceInitResult(False, RESOURCE_LOVELACE_UNAVAILABLE) resources = ( lovelace.resources if hasattr(lovelace, "resources") else lovelace["resources"] @@ -334,7 +380,7 @@ async def _init_resource(hass: HomeAssistant, url: str, ver: str) -> bool: if not isinstance(resources, ResourceStorageCollection): _LOGGER.debug("Add extra JS module (non-storage): %s", url2) add_extra_js_url(hass, url2) - return True + return ResourceInitResult(True, RESOURCE_EXTRA_MODULE) resources_obj = resources await resources_obj.async_get_info() @@ -349,7 +395,7 @@ async def _init_resource(hass: HomeAssistant, url: str, ver: str) -> bool: continue if item_url == url2 and item.get("res_type") == "module": - return True + return ResourceInitResult(True, RESOURCE_CURRENT) item_id = item.get("id") if not isinstance(item_id, str): @@ -360,12 +406,12 @@ async def _init_resource(hass: HomeAssistant, url: str, ver: str) -> bool: item_id, {"res_type": "module", "url": url2} ) - return True + return ResourceInitResult(True, RESOURCE_UPDATED) _LOGGER.debug("Add new lovelace resource: %s", url2) await resources_obj.async_create_item({"res_type": "module", "url": url2}) - return True + return ResourceInitResult(True, RESOURCE_CREATED) class WashDataCardRegistration: @@ -373,6 +419,9 @@ class WashDataCardRegistration: def __init__(self, hass: HomeAssistant) -> None: self.hass = hass + # Why the last attempt failed, so the caller's warning can say so without + # the user having to enable debug logging first (#432). + self.last_failure_reason: str = "unknown" def _src_path(self) -> Path: return Path(__file__).parent / "www" / CARD_NAME @@ -382,6 +431,7 @@ class WashDataCardRegistration: src = self._src_path() if not await self.hass.async_add_executor_job(src.exists): _LOGGER.warning("Card file not found: %s", src) + self.last_failure_reason = f"card file not found: {src}" return CARD_FAILED # Serve the minified build when it is provably current. The URL stays @@ -405,6 +455,7 @@ class WashDataCardRegistration: src, exc, ) + self.last_failure_reason = f"static path registration failed: {exc}" return CARD_FAILED version = await self.hass.async_add_executor_job(get_cache_buster) @@ -424,16 +475,36 @@ class WashDataCardRegistration: if unsubscribe_on_lovelace_loaded: unsubscribe_on_lovelace_loaded() try: - if await _init_resource(self.hass, INTEGRATION_URL, version): + outcome = await _init_resource( + self.hass, INTEGRATION_URL, version + ) + if outcome.ok: + self._log_success(outcome, version) self.hass.data["ha_washdata_card_registered"] = True self.hass.data["ha_washdata_card_deferred"] = False else: self.hass.data["ha_washdata_card_deferred"] = False - except Exception: # pylint: disable=broad-exception-caught + self.last_failure_reason = outcome.describe() + # Warn, not debug: this is the LAST chance to load the + # card, and nothing downstream is watching any more - + # async_register returned CARD_DEFERRED long ago, so a + # silent failure here left no evidence at all (#432). + _LOGGER.warning( + "Deferred card registration failed for %s: %s. The " + "WashData card will not be available until Home " + "Assistant is restarted.", + INTEGRATION_URL, + self.last_failure_reason, + ) + except Exception as exc: # pylint: disable=broad-exception-caught self.hass.data["ha_washdata_card_deferred"] = False - _LOGGER.debug( - "Delayed auto-registration of lovelace resource failed for %s", + self.last_failure_reason = str(exc) or type(exc).__name__ + _LOGGER.warning( + "Deferred card registration failed for %s: %s. The " + "WashData card will not be available until Home " + "Assistant is restarted.", INTEGRATION_URL, + self.last_failure_reason, ) unsubscribe_on_lovelace_loaded = self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, _on_lovelace_loaded) @@ -443,22 +514,27 @@ class WashDataCardRegistration: unsubscribe_on_lovelace_loaded() _LOGGER.debug("Lovelace already loaded after deferred listener; registering now") try: - if await _init_resource(self.hass, INTEGRATION_URL, version): + outcome = await _init_resource(self.hass, INTEGRATION_URL, version) + if outcome.ok: + self._log_success(outcome, version) self.hass.data["ha_washdata_card_registered"] = True self.hass.data["ha_washdata_card_deferred"] = False return CARD_REGISTERED self.hass.data["ha_washdata_card_deferred"] = False + self.last_failure_reason = outcome.describe() return CARD_FAILED - except Exception: # pylint: disable=broad-exception-caught + except Exception as exc: # pylint: disable=broad-exception-caught self.hass.data["ha_washdata_card_deferred"] = False + self.last_failure_reason = str(exc) or type(exc).__name__ return CARD_FAILED return CARD_DEFERRED # Lovelace is already loaded try: - registered = await _init_resource(self.hass, INTEGRATION_URL, version) + outcome = await _init_resource(self.hass, INTEGRATION_URL, version) except Exception as err: # pylint: disable=broad-exception-caught + self.last_failure_reason = str(err) or type(err).__name__ _LOGGER.debug( "Auto-registration of lovelace resource failed for %s: %s", INTEGRATION_URL, @@ -466,11 +542,37 @@ class WashDataCardRegistration: ) return CARD_FAILED - if registered: - _LOGGER.debug("Auto-registered lovelace resource for %s", INTEGRATION_URL) + if outcome.ok: + self._log_success(outcome, version) return CARD_REGISTERED + self.last_failure_reason = outcome.describe() return CARD_FAILED + def _log_success(self, outcome: ResourceInitResult, version: str) -> None: + """Name the mechanism that actually loaded the card (#432). + + The ``add_extra_js_url`` path creates no Lovelace resource, so reporting + it as "Auto-registered lovelace resource" sent anyone debugging a missing + card looking for a resource that was never going to exist - that is the + load path behind #384. It is logged at INFO because it is the one + outcome a user may need to know about (their resources are YAML-managed, + so the card will not appear in the Resources UI); the storage-backed + paths stay at debug, where they have always been. + """ + if outcome.mode == RESOURCE_EXTRA_MODULE: + _LOGGER.info( + "Card loaded as a frontend extra module (Lovelace resources are " + "YAML-managed, so no Lovelace resource was created): %s?v=%s", + INTEGRATION_URL, + version, + ) + else: + _LOGGER.debug( + "Auto-registered lovelace resource for %s (%s)", + INTEGRATION_URL, + outcome.mode, + ) + async def _async_register_path(hass: HomeAssistant, url_path: str, path: str) -> None: """Register one static path. diff --git a/custom_components/ha_washdata/manager.py b/custom_components/ha_washdata/manager.py index 11e5c3a3..4218b7af 100644 --- a/custom_components/ha_washdata/manager.py +++ b/custom_components/ha_washdata/manager.py @@ -27,7 +27,7 @@ import math import uuid import asyncio from asyncio import Task -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any, cast import numpy as np @@ -115,6 +115,10 @@ from .const import ( CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE, DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS, CONF_SMART_TERMINATION_DURATION_RATIO, + CONF_ANTI_CREASE_FINALIZE_RATIO, + CONF_CURVE_PREROLL_SECONDS, + DEFAULT_ANTI_CREASE_FINALIZE_RATIO, + DEFAULT_CURVE_PREROLL_SECONDS, DEFAULT_SMART_TERMINATION_DURATION_RATIO, DEFAULT_SMART_TERMINATION_DURATION_RATIO_BY_DEVICE, CONF_DELAY_START_DETECT_ENABLED, @@ -176,14 +180,20 @@ from .const import ( CONF_NOTIFY_LIVE_CHRONOMETER, CONF_NOTIFY_LIVE_STICKY, CONF_NOTIFY_LIVE_CLICK_ACTION, + CONF_NOTIFY_LIVE_SILENT, DEFAULT_NOTIFY_LIVE_STICKY, DEFAULT_NOTIFY_LIVE_CLICK_ACTION, + DEFAULT_NOTIFY_LIVE_SILENT, CONF_NOTIFY_REMINDER_MESSAGE, CONF_NOTIFY_TIMEOUT_SECONDS, CONF_NOTIFY_CHANNEL, CONF_NOTIFY_FINISH_CHANNEL, CONF_ENERGY_PRICE_STATIC, CONF_ENERGY_PRICE_ENTITY, + CONF_ENERGY_PRICE_DYNAMIC, + DEFAULT_ENERGY_PRICE_DYNAMIC, + PRICE_TIMELINE_MAX_POINTS, + PRICE_TIMELINE_PRICE_DECIMALS, CONF_ENERGY_SENSOR, CONF_PEAK_RATE_THRESHOLD, CONF_PEAK_RATE_MESSAGE, @@ -241,6 +251,8 @@ from .const import ( DEFAULT_END_REPEAT_COUNT, DEFAULT_MIN_OFF_GAP, DEFAULT_MIN_OFF_GAP_BY_DEVICE, + DEFAULT_UNMATCHED_WATCHDOG_CEILING, + DEFAULT_UNMATCHED_WATCHDOG_CEILING_BY_DEVICE, DEFAULT_MAX_DEFERRAL_SECONDS, ENDING_HARD_FINALIZE_MIN_QUIET_S, DEFAULT_START_ENERGY_THRESHOLDS_BY_DEVICE, @@ -273,15 +285,22 @@ from .profile_store import ( is_terminal_drop, terminal_drop_baseline, ) -from .signal_processing import integrate_wh, energy_gap_threshold_s +from .signal_processing import ( + integrate_wh, + energy_gap_threshold_s, + compact_price_timeline, + cycle_cost, +) from .recorder import CycleRecorder from .diag_buffer import DiagBuffer from .log_utils import DeviceLoggerAdapter +from .options_utils import option_float, option_int from .time_utils import power_data_to_offsets from . import analysis from . import progress as progress_mod from . import notification_rules as notif_rules from .phase_segmenter import phase_matching_enabled +from .frontend import PANEL_URL_PATH _LOGGER = logging.getLogger(__name__) @@ -313,6 +332,80 @@ _SENSOR_SWAP_BLOCKED_STATES = frozenset( } ) +# States in which a cycle is under way, so a manually picked program applies to it +# right now rather than being armed for the next one (#411). ANTI_WRINKLE is +# excluded on purpose: its tumble pulses belong to the cycle that already ended, +# so a program chosen there is meant for the next run. +_CYCLE_IN_PROGRESS_STATES = frozenset( + { + STATE_STARTING, + STATE_RUNNING, + STATE_PAUSED, + STATE_ENDING, + } +) + + +# Device classes and units that prove a configured "energy price entity" is not a +# price at all (#439). The panel's picker lists every `sensor.`, so the obvious +# mistake is to point it at the plug's own energy counter - and because a price +# entity outranks the static price, the cycle is then costed at +# `kWh_used * current_meter_reading`, which looks like a plausible number and is +# nonsense. A price is never measured in W or kWh; `monetary` and unitless or +# "EUR/kWh"-style sensors are left alone. +_NON_PRICE_DEVICE_CLASSES = frozenset( + {"energy", "energy_storage", "power", "gas", "water", "current", "voltage"} +) +_NON_PRICE_UNITS = frozenset( + {"w", "kw", "mw", "wh", "kwh", "mwh", "va", "kva", "varh", "a", "ma", "v", "mv"} +) + + +def _finite_power(raw: Any) -> float | None: + """Parse a power sensor's state string, rejecting non-finite values. + + ``float()`` accepts ``"nan"``, ``"inf"`` and ``"infinity"``, and a power + reading is compared against thresholds exactly like the options + ``options_utils.option_float`` already guards: every comparison against + ``nan`` is False, so a single such reading does not raise, it silently + switches gates OFF. With ``_current_power`` set to ``nan`` both the + unmatched-cycle watchdog (`< start_threshold_w`) and the high-power silence + deferral (`> min_power`) evaluate False, so a running cycle loses the guards + that decide whether it ends at all. + + Returns None for anything unusable, which is the "sensor is non-numeric" + outcome every caller already handles. A real meter never reports either + value, so no valid reading changes behaviour. + """ + try: + power = float(raw) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(power): + return None + return power + + +def _coerce_price_timeline(raw: Any) -> list[tuple[float, float]]: + """Coerce a persisted price timeline back into ``(ts, price)`` tuples (#426). + + JSON round-trips the pairs as lists, and a hand-edited or truncated store must + not be able to break cycle restoration, so anything unparseable is dropped + rather than raised on. + """ + result: list[tuple[float, float]] = [] + if not isinstance(raw, (list, tuple)): + return result + for entry in raw: + if not isinstance(entry, (list, tuple)) or len(entry) < 2: + continue + try: + result.append((float(entry[0]), float(entry[1]))) + except (TypeError, ValueError): + continue + result.sort(key=lambda item: item[0]) + return result + def _sanitize_ranking(raw_list: list[dict[str, Any]], limit: int = 5) -> list[dict[str, Any]]: """Top-N ranking candidates stripped of the heavy `current`/`sample` power @@ -340,9 +433,12 @@ _MOBILE_ONLY_EXTRA_KEYS = ( "actions", "sticky", "clickAction", + "url", "subtitle", "content_state", "activity", + "silent", + "push", ) @@ -444,6 +540,7 @@ class WashDataManager: self._notify_live_chronometer = DEFAULT_NOTIFY_LIVE_CHRONOMETER self._notify_live_sticky = DEFAULT_NOTIFY_LIVE_STICKY self._notify_live_click_action = DEFAULT_NOTIFY_LIVE_CLICK_ACTION + self._notify_live_silent = DEFAULT_NOTIFY_LIVE_SILENT self._notify_timeout_seconds = DEFAULT_NOTIFY_TIMEOUT_SECONDS self._pending_notifications: list[dict[str, Any]] = [] # Quiet-hours (do-not-disturb) hold queue + release timer. Finish-type @@ -471,6 +568,20 @@ class WashDataManager: self._energy_meter_start: float | None = None self._energy_meter_source: str | None = None + # Dynamic energy price timeline for the current cycle (#426): the price in + # force at each point of the cycle, as ``(unix_ts, price_per_kwh)`` pairs, + # appended by the price-entity listener below. Absolute timestamps, not + # offsets: the stored cycle's start time can end up later than the detector's + # (leading-zero trim, a split), and converting once at cycle end against the + # figure actually persisted is the only way the two axes cannot drift apart. + # Survives a restart via the active-cycle snapshot; any price change that + # happened while HA was down is recovered from the recorder at cycle end. + self._price_timeline: list[tuple[float, float]] = [] + self._remove_price_listener: Callable[[], None] | None = None + # Entity id already reported as "not a price" (#439), so the rejection is + # logged once per misconfiguration instead of on every cycle. + self._warned_price_entity: str | None = None + # Pause tracking (user-triggered) self._user_pause_start: datetime | None = None self._total_user_paused_seconds: float = 0.0 @@ -634,14 +745,31 @@ class WashDataManager: config_entry.options.get(CONF_LOW_POWER_NO_UPDATE_TIMEOUT, 3600.0) ) self._off_delay = float(config_entry.options.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY)) - self._learning_confidence = config_entry.options.get( - CONF_LEARNING_CONFIDENCE, DEFAULT_LEARNING_CONFIDENCE + # Device-scaled ceiling for the unmatched (expected == 0) zombie guard (#404). + # Not a user option; purely a function of device_type, so it is recomputed + # alongside device_type on reconfigure. + self._unmatched_watchdog_ceiling = float( + DEFAULT_UNMATCHED_WATCHDOG_CEILING_BY_DEVICE.get( + self.device_type, DEFAULT_UNMATCHED_WATCHDOG_CEILING + ) + ) + # Coerced here rather than at the point of use. Both thresholds are compared + # against a match confidence inside the cycle-end tail, and that tail runs as + # a spawned task: a non-numeric option (an import file is hand-editable, and + # strip_null_options only removes nulls) raised there instead, killing the task + # before async_add_cycle and losing the whole cycle. Same #389 failure shape, + # one step later. Falls back to the default rather than to 0, which would + # silently auto-label everything. + self._learning_confidence = option_float( + config_entry.options.get(CONF_LEARNING_CONFIDENCE, DEFAULT_LEARNING_CONFIDENCE), + DEFAULT_LEARNING_CONFIDENCE, ) self._duration_tolerance = config_entry.options.get( CONF_DURATION_TOLERANCE, DEFAULT_DURATION_TOLERANCE ) - self._auto_label_confidence = config_entry.options.get( - CONF_AUTO_LABEL_CONFIDENCE, DEFAULT_AUTO_LABEL_CONFIDENCE + self._auto_label_confidence = option_float( + config_entry.options.get(CONF_AUTO_LABEL_CONFIDENCE, DEFAULT_AUTO_LABEL_CONFIDENCE), + DEFAULT_AUTO_LABEL_CONFIDENCE, ) self._profile_match_interval = int( @@ -698,6 +826,11 @@ class WashDataManager: ) or "" ).strip() + self._notify_live_silent = bool( + config_entry.options.get( + CONF_NOTIFY_LIVE_SILENT, DEFAULT_NOTIFY_LIVE_SILENT + ) + ) self._notify_timeout_seconds = int( config_entry.options.get( CONF_NOTIFY_TIMEOUT_SECONDS, DEFAULT_NOTIFY_TIMEOUT_SECONDS @@ -844,6 +977,22 @@ class WashDataManager: resolve_smart_termination_duration_ratio_default(self.device_type), ) ), + # #429: same reasoning, different gate - this one gates the finalise + # into STATE_ANTI_WRINKLE, not Smart Termination. Scalar default: the + # safe value is per-machine, not per-device-type. + anti_crease_finalize_ratio=float( + config_entry.options.get( + CONF_ANTI_CREASE_FINALIZE_RATIO, + DEFAULT_ANTI_CREASE_FINALIZE_RATIO, + ) + ), + # #430: 0 = off, which is the default and leaves the stored-duration + # convention exactly as it was. + curve_preroll_seconds=float( + config_entry.options.get( + CONF_CURVE_PREROLL_SECONDS, DEFAULT_CURVE_PREROLL_SECONDS + ) + ), delay_detect_enabled=bool( config_entry.options.get( CONF_DELAY_START_DETECT_ENABLED, DEFAULT_DELAY_START_DETECT_ENABLED @@ -896,11 +1045,34 @@ class WashDataManager: self._current_program, elapsed_seconds, ) + # Elements 9 and 10 matter even here. update_match CLEARS + # _matched_tail_power and _matched_terminal_high for any tuple + # shorter than this, on the sound reasoning that a newly matched + # profile must not inherit the previous one's tail - but a manual + # pin names its profile, so the answer is to supply that profile's + # own values rather than nothing. Left empty, the #364 tail guard + # and the #399 anti-crease spin wait both sat inert for every + # hand-picked program, so a washer could finalize in the quiet + # before its terminal spin and record the spin as a second cycle. + # Elements 5-8 stay False: a manual pin is certain by definition, + # so there is no mismatch or ambiguity to report. + terminal_high = None + if self.detector.config.anti_wrinkle_enabled: + terminal_high = self.profile_store.profile_terminal_high_block( + self._current_program, + self.detector.config.anti_wrinkle_max_power, + ) return ( self._current_program, 1.0, expected_duration, manual_phase or "Manual", + False, + False, + False, + False, + self.profile_store.profile_tail_power(self._current_program), + terminal_high, ) if not readings: @@ -942,8 +1114,18 @@ class WashDataManager: resolve_watchdog_interval_default(self.device_type), ) ) - self._match_persistence = int( - config_entry.options.get(CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE) + # option_int, not a bare int(): this runs in __init__, so a hand-edited + # import putting a non-numeric or oversized value here raised before the + # manager existed and the entry could never finish setup. The floor of 1 is + # what `SuggestionEngine` already applies to the same key - and it has to, + # because its interval cap is computed FROM this number, so an unclamped 0 + # here would have the suggestion describe a persistence the matcher is not + # using. Zero also disables the gate rather than tightening it: every + # `counter >= 0` is true, so the first match commits. + self._match_persistence = option_int( + config_entry.options.get(CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE), + DEFAULT_MATCH_PERSISTENCE, + minimum=1, ) self._sampling_interval = float( config_entry.options.get( @@ -974,6 +1156,10 @@ class WashDataManager: # and frozen onto the cycle at end for panel badging. self._cycle_anomaly: str = "none" self._overrun_ratio: float = 0.0 + # Where this run maps onto its matched profile's envelope, 0-1, from the + # DTW alignment that already runs for the verified-pause decision. Visible + # only: nothing reads it back. None until an alignment has produced one. + self._envelope_position: float | None = None # Post-cycle anomaly cache: holds energy/underrun anomaly from the last # completed cycle so sensor attributes can surface them after idle. self._last_cycle_post_anomaly: dict = {} @@ -987,6 +1173,12 @@ class WashDataManager: self._last_match_ambiguous: bool = False self._matched_profile_duration: float | None = None self._last_match_confidence: float = 0.0 + # Stage-5 companion to the above (item 206): what the SELECTED group member + # earned on its own curve, vs the group's score in _last_match_confidence. + # None for every non-group match, and the label gate then behaves exactly as + # before. Kept as a separate field rather than replacing the confidence, + # because the confidence is what the detector's end-detection gates read. + self._last_member_confidence: float | None = None # Sample interval tracking (seconds) for adaptive timing # Profile matching duration tolerance (0.25 = ±25%) self._profile_duration_tolerance: float = float( @@ -1009,6 +1201,9 @@ class WashDataManager: self._pump_stuck: bool = False # True once the stuck threshold has fired for this cycle self._manual_program_active: bool = False + # The user's standing program choice for the current or next cycle (#411). + # Rehydrated from the store during setup so arming survives a restart. + self._armed_program: str | None = None self._notified_start: bool = False self._notified_pre_completion: bool = False self._last_match_result: Any = None # Stores full MatchResult object @@ -1054,9 +1249,15 @@ class WashDataManager: current_duration = (end_time - start_time).total_seconds() # 1. RUN BETTER ASYNC MATCHING + # in_progress: this is the live match on a cycle that is still running, + # so Stage 4 grades each candidate on the same elapsed stretch instead + # of on its complete duration/energy (#400). The two final-match paths + # (_run_final_match_from_cycle_data, _async_process_cycle_end) leave it + # off - there the cycle really is complete. result = await self.profile_store.async_match_profile( readings, - current_duration + current_duration, + in_progress=True, ) # 2. UPDATE MANAGER STATE (Estimates, Program Name, etc.) @@ -1282,12 +1483,13 @@ class WashDataManager: else: self._current_program = profile_name self._last_match_confidence = confidence + self._last_member_confidence = result.member_confidence self._unmatch_persistence_counter = 0 # Reset on switch if profile_name in self._match_persistence_counter: self._match_persistence_counter[profile_name] = self._match_persistence # Lock it in - avg_duration = float(matched_duration) - self._matched_profile_duration = avg_duration if avg_duration > 0 else None + self._matched_profile_duration = self._profile_duration(matched_duration) + avg_duration = self._matched_profile_duration or 0.0 self._logger.info( "Switching to profile '%s' (reason: %s). Expected duration: %.0fs (%smin)", profile_name, switch_reason, avg_duration, int(avg_duration / 60), @@ -1295,6 +1497,7 @@ class WashDataManager: elif profile_name == self._current_program: # Same program, but update confidence for sensors self._last_match_confidence = confidence + self._last_member_confidence = result.member_confidence elif not self._matched_profile_duration: self._current_program = "detecting..." @@ -1353,6 +1556,15 @@ class WashDataManager: # unreachable and the cycle would hang to the deferral cap (#348). try: span = self.profile_store.envelope_time_span(current_matched) + if span > 0: + # The same ratio the release below tests, kept for the + # state attribute. It is the only continuous "how far + # through this programme are we" figure the integration + # has that is independent of elapsed time, so it stays + # meaningful when a run over- or under-shoots its mean. + self._envelope_position = round( + min(1.0, max(0.0, mapped_time / span)), 3 + ) if span > 0 and (mapped_time / span) > 0.95: verified_pause = False self._logger.info( @@ -1445,11 +1657,14 @@ class WashDataManager: if profile_name: self._current_program = profile_name self._last_match_confidence = confidence + self._last_member_confidence = result.member_confidence # Try to fetch duration if we switched back to matched try: prof = self.profile_store.get_profile(profile_name) if prof: - self._matched_profile_duration = float(prof.get("avg_duration", 0)) + self._matched_profile_duration = self._profile_duration( + prof.get("avg_duration") + ) except Exception as e: self._logger.debug("Failed to fetch profile duration on switch: %s", e) else: @@ -1468,14 +1683,22 @@ class WashDataManager: # Push updates to detector self.detector.set_verified_pause(verified_pause) # Element 8 is the narrow #288-only prefix verdict and element 9 the - # matched profile's own tail power level, both for the #364 guards. The - # detector tolerates shorter tuples, so other callers stay valid. + # matched profile's own tail power level, both for the #364 guards; + # element 10 is its terminal high-power block for the #399 anti-crease + # guard. The detector tolerates shorter tuples, so other callers stay + # valid. + terminal_high = None + if profile_name and self.detector.config.anti_wrinkle_enabled: + terminal_high = self.profile_store.profile_terminal_high_block( + profile_name, self.detector.config.anti_wrinkle_max_power + ) self.detector.update_match( (profile_name, confidence, matched_duration, phase_name, result.is_confident_mismatch, result.is_ambiguous, result.is_prefix_ambiguous, result.is_prefix_ambiguous_full_shape, - self.profile_store.profile_tail_power(profile_name) if profile_name else None) + self.profile_store.profile_tail_power(profile_name) if profile_name else None, + terminal_high) ) # --- LOGGING (Unified) --- @@ -1622,17 +1845,20 @@ class WashDataManager: power_is_valid = False if state and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - current_power = float(state.state) - power_is_valid = True - except (ValueError, TypeError): - # Power sensor state is not numeric during restoration; treat as 0W + _restore_power = _finite_power(state.state) + if _restore_power is None: + # Not numeric, or nan/inf: treat as 0W and do not restore by power. + # Leaving power_is_valid False is what keeps a non-finite reading out + # of the restore decision, whose comparisons would silently be False. self._logger.debug( - "Power sensor %s state %r is not numeric during restoration; " - "treating as 0W and not restoring by power", + "Power sensor %s state %r is not a finite number during " + "restoration; treating as 0W and not restoring by power", self.power_sensor_entity_id, getattr(state, "state", None), ) + else: + current_power = _restore_power + power_is_valid = True should_restore = False active_snapshot_to_restore: dict[str, Any] | None = ( @@ -1807,6 +2033,14 @@ class WashDataManager: "energy_meter_source" ) + # Restore the dynamic price timeline (#426). Prices recorded + # before the restart stay valid; a change that happened while HA + # was down is recovered from the recorder at cycle end, guided by + # the restart gap recorded a few lines below. + self._price_timeline = _coerce_price_timeline( + active_snapshot_to_restore.get("price_timeline") + ) + # If we restored into a low-power state, ensure we don't # immediately quit. For now we just log this; the cycle # detector's off_delay will handle actual shutdown. @@ -1827,7 +2061,44 @@ class WashDataManager: ) else: self._current_program = "detecting..." - + + # Re-pin a manual program override across the restart (#404 + # secondary bug). _manual_program_active was restored above, but + # _matched_profile_duration was not; without this the manual + # matcher tuple would feed expected_duration=0 on the next tick, + # wipe the detector's matched_profile, and drop the cycle to + # "detecting..." + the unmatched watchdog guard. The duration is + # re-read from the (possibly since-learned) profile, so an empty + # profile stays active with a None duration rather than being lost. + if self._manual_program_active: + manual_name = ( + active_snapshot_to_restore.get("manual_program_name") + or self.detector.matched_profile + ) + profile = ( + self.profile_store.get_profile(manual_name) + if manual_name + else None + ) + if profile is not None: + self._current_program = manual_name + self._matched_profile_duration = self._profile_duration( + profile.get("avg_duration") + ) + self._logger.info( + "Restored manual program override: %s (duration=%.0fs)", + manual_name, + self._matched_profile_duration or 0.0, + ) + else: + # The chosen profile was deleted while HA was down. + self._manual_program_active = False + self._logger.info( + "Manual program %r no longer exists after restart; " + "reverting to auto-detect", + manual_name, + ) + # Restore persisted start-notification/event flags from snapshot. self._notified_start = bool( active_snapshot_to_restore.get("notified_start", False) @@ -1992,15 +2263,46 @@ class WashDataManager: # Load recorder state await self.recorder.async_load() + # Rehydrate a program armed before a restart (#411). Arming is an + # idle-time action, so the wait between picking a program and starting the + # machine can easily span a Home Assistant restart. + try: + self._armed_program = self.profile_store.get_armed_program() + if self._armed_program: + self._logger.info( + "Program %r is armed for the next cycle", self._armed_program + ) + except Exception: # pylint: disable=broad-exception-caught + self._armed_program = None + # Force initial update from current state (in case it's already stable) state = self.hass.states.get(self.power_sensor_entity_id) if state and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - power = float(state.state) - now = dt_util.now() - self.detector.process_reading(power, now) - except (ValueError, TypeError): - pass + # A non-finite reading is skipped entirely, which leaves the cache + # unset, i.e. the pre-#409 behaviour. Seeding it with a nan instead + # would be PERMANENT: _resync_power_from_state returns early precisely + # when the sensor is non-finite, so the healing path could never + # overwrite it, and every later watchdog comparison would be False. + power = _finite_power(state.state) + if power is not None: + try: + now = dt_util.now() + self.detector.process_reading(power, now) + # Seed the reading cache from the sensor itself (#409). The + # reading was already fed to the detector; leaving the manager's + # own cache unset meant every reload started with + # _current_power = 0 and _last_reading_time = None, so (a) the + # power tile/entity reported a value the sensor never had until + # the next event and (b) the watchdog - which returns early while + # _last_reading_time is None - could neither keepalive nor close a + # restored cycle whose plug went silent across the reload. + self._current_power = power + self._last_reading_time = now + self._last_real_reading_time = ( + getattr(state, "last_reported", None) or state.last_updated + ) + except (ValueError, TypeError): + pass # Trigger migration/compression of old cycle format # This is safe to run repeatedly (it skips already compressed cycles) @@ -2017,6 +2319,9 @@ class WashDataManager: # Subscribe to door sensor (if configured) await self._setup_door_sensor_listener() + # Subscribe to the dynamic energy price entity (if configured, #426) + await self._setup_price_listener() + # Subscribe to person presence changes for notification gating await self._setup_notify_people_listener() @@ -2095,12 +2400,13 @@ class WashDataManager: # Force update from new sensor state = self.hass.states.get(self.power_sensor_entity_id) if state and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - power = float(state.state) - self.detector.process_reading(power, dt_util.now()) - except ValueError: + _reload_power = _finite_power(state.state) + if _reload_power is not None: + self.detector.process_reading(_reload_power, dt_util.now()) + else: self._logger.debug( - "Initial power value for %s after config reload is not numeric: %r", + "Initial power value for %s after config reload is not a " + "finite number: %r", self.power_sensor_entity_id, state.state, ) @@ -2113,6 +2419,13 @@ class WashDataManager: # Propagate to learning pipeline (captured at construction time) self.learning_manager.device_type = self.device_type self.learning_manager.suggestion_engine.device_type = self.device_type + # Recompute the device-scaled unmatched-guard ceiling (#404): it tracks + # device_type, which the reconfigure flow can change. + self._unmatched_watchdog_ceiling = float( + DEFAULT_UNMATCHED_WATCHDOG_CEILING_BY_DEVICE.get( + self.device_type, DEFAULT_UNMATCHED_WATCHDOG_CEILING + ) + ) # Update detector config in-place old_min_power = self.detector.config.min_power @@ -2148,6 +2461,12 @@ class WashDataManager: self.profile_store.dtw_bandwidth = float( config_entry.options.get(CONF_DTW_BANDWIDTH, DEFAULT_DTW_BANDWIDTH) ) + # Re-plumbed on every reload, not just at construction (issue #407): this is + # a panel checkbox, and an options change reloads in place without rebuilding + # the store, so without this line the toggle only took effect on an HA restart. + self.profile_store.save_debug_traces = config_entry.options.get( + CONF_SAVE_DEBUG_TRACES, False + ) # Stage-4 energy discriminator: integrated energy for WM/washer-dryer, # mean power elsewhere (see analysis.stage4_energy_mode). self.profile_store.energy_mode = analysis.stage4_energy_mode(self.device_type) @@ -2256,6 +2575,17 @@ class WashDataManager: resolve_smart_termination_duration_ratio_default(self.device_type), ) ) + new_anti_crease_finalize_ratio = float( + config_entry.options.get( + CONF_ANTI_CREASE_FINALIZE_RATIO, + DEFAULT_ANTI_CREASE_FINALIZE_RATIO, + ) + ) + new_curve_preroll_seconds = float( + config_entry.options.get( + CONF_CURVE_PREROLL_SECONDS, DEFAULT_CURVE_PREROLL_SECONDS + ) + ) new_delay_detect_enabled = bool( config_entry.options.get( CONF_DELAY_START_DETECT_ENABLED, DEFAULT_DELAY_START_DETECT_ENABLED @@ -2296,6 +2626,8 @@ class WashDataManager: self.detector.config.anti_wrinkle_idle_timeout = new_anti_wrinkle_idle_timeout self.detector.config.dishwasher_end_spike_quiet_release = new_dishwasher_end_spike_quiet_release self.detector.config.smart_termination_duration_ratio = new_smart_termination_duration_ratio + self.detector.config.anti_crease_finalize_ratio = new_anti_crease_finalize_ratio + self.detector.config.curve_preroll_seconds = new_curve_preroll_seconds self.detector.config.delay_detect_enabled = new_delay_detect_enabled self.detector.config.delay_confirm_seconds = new_delay_confirm_seconds self.detector.config.delay_timeout_seconds = new_delay_timeout_seconds @@ -2428,6 +2760,11 @@ class WashDataManager: ) or "" ).strip() + self._notify_live_silent = bool( + config_entry.options.get( + CONF_NOTIFY_LIVE_SILENT, DEFAULT_NOTIFY_LIVE_SILENT + ) + ) self._notify_timeout_seconds = int( config_entry.options.get( CONF_NOTIFY_TIMEOUT_SECONDS, DEFAULT_NOTIFY_TIMEOUT_SECONDS @@ -2465,6 +2802,11 @@ class WashDataManager: await self._setup_door_sensor_listener() self._maybe_arm_door_end_dwell_if_open() + # Re-subscribe to the dynamic energy price entity (#426). A changed entity + # (or the toggle being turned off) takes effect from here on; the samples + # already recorded for a running cycle stay - they were true when taken. + await self._setup_price_listener() + # Re-subscribe to person presence changes for notification gating await self._setup_notify_people_listener() @@ -2574,6 +2916,9 @@ class WashDataManager: if self._remove_door_sensor_listener: self._remove_door_sensor_listener() self._remove_door_sensor_listener = None + if self._remove_price_listener: + self._remove_price_listener() + self._remove_price_listener = None self._cancel_door_end_dwell() # Drop the repeat-unload-reminder dismiss action listener (#374). self._remove_unload_dismiss_listener() @@ -2669,6 +3014,101 @@ class WashDataManager: self.hass, [entity_id], self._handle_door_sensor_change ) + async def _setup_price_listener(self) -> None: + """Subscribe to the dynamic energy price entity (#426). + + Only when a price *entity* is configured and dynamic pricing is on: a + static price cannot move, so there is nothing to track. Registered for the + entity's whole lifetime rather than per cycle - the appended samples are + gated on the detector being active, and a subscription that only exists + while a cycle runs would miss the price in force at the moment it starts. + """ + if self._remove_price_listener: + self._remove_price_listener() + self._remove_price_listener = None + + if not self._dynamic_pricing_enabled(): + return + + entity_id = self._price_entity_id() + if not entity_id: + return + self._logger.debug("Setting up dynamic price listener: %s", entity_id) + self._remove_price_listener = async_track_state_change_event( + self.hass, [entity_id], self._handle_price_change + ) + + def _dynamic_pricing_enabled(self) -> bool: + """Whether cost should be integrated against a moving price (#426).""" + options = self.config_entry.options + if not self._price_entity_id(): + return False + return bool( + options.get(CONF_ENERGY_PRICE_DYNAMIC, DEFAULT_ENERGY_PRICE_DYNAMIC) + ) + + @callback + def _handle_price_change(self, event: Event[evt.EventStateChangedData]) -> None: + """Record a price change onto the running cycle's timeline (#426).""" + new_state = event.data.get("new_state") + if new_state is None: + return + try: + price = float(new_state.state) + except (ValueError, TypeError): + # unknown/unavailable/non-numeric: carry the last known price forward + # rather than charging the cycle at zero for the outage. + return + self._append_price_sample(price) + + def _append_price_sample(self, price: float | None) -> None: + """Append ``price`` to the current cycle's timeline, deduplicated. + + No-op when no cycle is running - the timeline describes one cycle - and + when the price is unchanged, so a template sensor that re-emits the same + number every few seconds costs one comparison and nothing else. + """ + if price is None: + return + if self.detector.state not in ( + STATE_STARTING, + STATE_RUNNING, + STATE_PAUSED, + STATE_ENDING, + ): + return + try: + value = round(float(price), PRICE_TIMELINE_PRICE_DECIMALS) + except (ValueError, TypeError): + return + if not math.isfinite(value): + # Same rule as _finite_power: "nan"/"inf" parse cleanly and would ride + # into the stored timeline. nan also defeats the dedup below, since it + # compares unequal to itself, so every report would append a point. + return + if self._price_timeline and self._price_timeline[-1][1] == value: + return + self._price_timeline.append((dt_util.now().timestamp(), value)) + # Hard bound so a pathologically chatty price entity cannot grow the + # in-memory list without limit during a long cycle; the stored timeline is + # compacted again (by price step) at cycle end. + if len(self._price_timeline) > PRICE_TIMELINE_MAX_POINTS * 4: + self._price_timeline = [ + (offset, price_val) + for offset, price_val in compact_price_timeline( + self._price_timeline, + max_points=PRICE_TIMELINE_MAX_POINTS * 2, + decimals=PRICE_TIMELINE_PRICE_DECIMALS, + ) + ] + + def _start_price_timeline(self) -> None: + """Open a fresh price timeline for a cycle that just started (#426).""" + self._price_timeline = [] + if not self._dynamic_pricing_enabled(): + return + self._append_price_sample(self._resolve_energy_price()) + @callback def _handle_door_sensor_change(self, event: Event[evt.EventStateChangedData]) -> None: """Handle door sensor state changes. @@ -2925,6 +3365,23 @@ class WashDataManager: self.detector.user_stop() self._logger.info("Cycle completed via external trigger") + def _external_end_trigger_available(self) -> bool: + """True when an authoritative external end trigger is wired and reporting. + + Used by the unmatched zombie guard (#404): if the user has configured an + external end-of-cycle binary sensor and it currently has a usable state, an + authoritative end signal already exists, so the time-based failsafe would only + do harm (it would truncate a long programme that the trigger will end cleanly). + Returns False if disabled, unconfigured, or the entity is missing/unavailable. + """ + if not self.config_entry.options.get(CONF_EXTERNAL_END_TRIGGER_ENABLED, False): + return False + entity_id = self.config_entry.options.get(CONF_EXTERNAL_END_TRIGGER, "") + if not entity_id: + return False + state = self.hass.states.get(entity_id) + return state is not None and state.state not in ("unavailable", "unknown") + async def _setup_maintenance_scheduler(self) -> None: """Set up daily maintenance task at midnight.""" auto_maintenance = self.config_entry.options.get( @@ -3199,6 +3656,116 @@ class WashDataManager: await self.profile_store.async_save() return int(result.get("evaluated_count", 0)) + async def async_recompute_cycle_costs(self) -> int: + """Recost stored cycles from the recorder's price history (#426). + + Existing cycles were costed at the single price in force when they ended - + including everything imported from raw history (#344), which was costed at + whatever the tariff happened to be at import time. Where the recorder still + holds the price entity's history, those cycles can be recosted properly + after the fact. Returns the number of cycles rewritten. + + Deliberately conservative, because it overwrites a figure the user has + already seen: + + * only ``past_cycles`` and ``backfill_cycles`` - reference cycles are other + people's recordings and were never the user's energy to pay for; + * only cycles the recorder can actually answer for (a price row at or before + the cycle's start); anything older than the recorder's retention keeps the + cost it has; + * cycles already costed dynamically are left alone, so the pass is + idempotent and never re-derives a live-tracked timeline from a coarser + recorder view. + """ + if not self._dynamic_pricing_enabled(): + return 0 + candidates: list[tuple[dict[str, Any], datetime, datetime]] = [] + for cycle in list(self.profile_store.get_past_cycles()) + list( + self.profile_store.get_backfill_cycles() + ): + if cycle.get("energy_price_mode") == "dynamic" and cycle.get("price_timeline"): + continue + start_dt = dt_util.parse_datetime(str(cycle.get("start_time") or "")) + end_dt = dt_util.parse_datetime(str(cycle.get("end_time") or "")) + # An ISO string without an offset parses naive, which every record this + # device writes is not, but an import or a hand-edited file can be. Read + # as UTC, the same way the odometer scan does: one such cycle otherwise + # raises on the aware `horizon` comparison below, and the WS caller only + # debug-logs that, so the whole pass silently recosts nothing. + if start_dt is not None and start_dt.tzinfo is None: + start_dt = start_dt.replace(tzinfo=dt_util.UTC) + if end_dt is not None and end_dt.tzinfo is None: + end_dt = end_dt.replace(tzinfo=dt_util.UTC) + if start_dt is None or end_dt is None or end_dt <= start_dt: + continue + candidates.append((cycle, start_dt, end_dt)) + if not candidates: + return 0 + + # Bound the query to what the recorder can still answer. Reading further + # back returns nothing but makes the executor walk the whole retained + # window of a frequently-updating price entity for it. + keep_days = 10.0 + try: + from homeassistant.components.recorder import get_instance # noqa: PLC0415 + + keep_days = float(getattr(get_instance(self.hass), "keep_days", 10) or 10) + except Exception: # noqa: BLE001 - recorder optional; the default stands + pass + horizon = dt_util.now() - timedelta(days=min(max(keep_days, 1.0), 365.0)) + candidates = [c for c in candidates if c[2] >= horizon] + if not candidates: + return 0 + + window_start = min(start for _, start, _ in candidates) + window_end = max(end for _, _, end in candidates) + rows = await self._async_price_history(max(window_start, horizon), window_end) + if not rows: + return 0 + + updated = 0 + for cycle, start_dt, end_dt in candidates: + start_ts = start_dt.timestamp() + end_ts = end_dt.timestamp() + # Require an anchor at or before the cycle: without one the first known + # price would be back-applied to energy bought before it existed. + anchor: tuple[float, float] | None = None + window: list[tuple[float, float]] = [] + for ts, price in rows: + if ts <= start_ts: + # Keep only the newest pre-start row. Older ones all collapse + # onto offset 0 and, once there are more of them than + # PRICE_TIMELINE_MAX_POINTS, compaction can spend the whole + # budget on prices this cycle never ran at and evict the real + # anchor or an in-cycle transition. + anchor = (ts, price) + elif ts <= end_ts: + window.append((ts, price)) + if anchor is None: + continue + window.insert(0, anchor) + points = compact_price_timeline( + [(max(0.0, ts - start_ts), price) for ts, price in window], + max_points=PRICE_TIMELINE_MAX_POINTS, + decimals=PRICE_TIMELINE_PRICE_DECIMALS, + ) + if not points: + continue + result = self._cost_from_timeline(cycle, points) + if result is None: + continue + cost, effective_price = result + cycle["cost"] = round(cost, 4) + cycle["energy_price"] = round(effective_price, 6) + cycle["energy_price_mode"] = "dynamic" + cycle["price_timeline"] = [[round(offset, 1), price] for offset, price in points] + updated += 1 + + if updated: + await self.profile_store.async_save() + self._logger.info("Recosted %d cycle(s) from recorder price history", updated) + return updated + def _last_ml_training_at(self) -> datetime | None: """When on-device training last *ran* (not just last promoted a model). @@ -3260,9 +3827,8 @@ class WashDataManager: if new_state is None or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): return - try: - power = float(new_state.state) - except ValueError: + power = _finite_power(new_state.state) + if power is None: return # Capture every raw sensor reading before any throttling or processing. @@ -3302,10 +3868,9 @@ class WashDataManager: if old_state is not None and old_state.state not in ( STATE_UNKNOWN, STATE_UNAVAILABLE ): - try: - prev_raw_power = float(old_state.state) - except ValueError: - pass + _prev = _finite_power(old_state.state) + if _prev is not None: + prev_raw_power = _prev is_low_power = power < min_p and ( self.detector.state in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING) or prev_raw_power >= min_p # genuine drop from active power @@ -3410,6 +3975,7 @@ class WashDataManager: ) self._current_program = profile_name self._last_match_confidence = confidence + self._last_member_confidence = result.member_confidence else: self._logger.info( "No confident match from cycle data (best: %s, conf=%.3f)", @@ -3490,7 +4056,7 @@ class WashDataManager: (now - last_real).total_seconds(), self._off_delay, ) - self.detector.process_reading(0.0, now) + self.detector.process_reading(0.0, now, synthetic=True) self._notify_update() return if ( @@ -3501,6 +4067,14 @@ class WashDataManager: # Cycle is running or not completed, don't reset return + # Keep the cached power honest in the terminal states too (#409). The + # watchdog is stopped here, so nothing else refreshes it: the panel/entity + # would keep reporting the last value seen before the cycle ended, and + # power-based Off detection (#284) - which compares that same cache against + # power_off_threshold_w - could never see the appliance being switched off. + # Display/decision cache only; the detector is not fed in a terminal state. + self._resync_power_from_state(now, feed_detector=False) + time_since_complete = (now - self._cycle_completed_time).total_seconds() # Clean laundry nag notification. In repeat mode (#374) the reminder re-fires @@ -3743,6 +4317,69 @@ class WashDataManager: ) self._reset_terminal_to_off() + def _live_power_state(self) -> tuple[float, datetime] | None: + """Return ``(power, report_ts)`` from the power sensor's CURRENT state. + + ``hass.states`` is the authoritative record of what the plug last said, and + unlike the manager's own cache it cannot go stale: it is written on every + report, including the ones this manager deliberately drops (the + sampling-interval throttle) or never receives (an event lost across a + reload). ``last_reported`` advances on every write - unchanged re-reports + included - so it is the honest "when did the sensor last speak" clock. + + Returns None when the sensor is missing, unavailable or non-numeric. + """ + state = self.hass.states.get(self.power_sensor_entity_id) + if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + return None + power = _finite_power(state.state) + if power is None: + return None + report_ts = getattr(state, "last_reported", None) or state.last_updated + if not isinstance(report_ts, datetime): + return None + return power, report_ts + + def _resync_power_from_state(self, now: datetime, feed_detector: bool) -> None: + """Re-anchor the cached power on the sensor's live state (#409). + + ``_current_power`` is otherwise a pure event cache: one dropped or missed + state event and it stays wrong indefinitely, because nothing ever compares + it against the sensor again. That single stale number then drives the + watchdog's high-power branch, the low-power keepalive gate and the #284 + power-off detection, so the divergence does not merely show a wrong value + in the panel - it decides whether a cycle ends at all. + + Called from the two periodic timers (the in-cycle watchdog and the terminal + state-expiry poll), so the cache can never be more than one tick out of + step with the sensor. When ``feed_detector`` is set and the sensor has + reported since our last real reading, that report is also handed to the + detector: it is a genuine observation we simply never processed. Timestamped + with ``now`` rather than the report time so the detector's dt can never run + backwards (a negative dt is discarded, taking the reading with it). + """ + live = self._live_power_state() + if live is None: + return + power, report_ts = live + missed = ( + self._last_real_reading_time is None + or report_ts > self._last_real_reading_time + ) + if feed_detector and missed: + self._logger.debug( + "Resync: sensor reported %.2fW at %s but the last processed reading " + "was %s (cached %.2fW); processing it now", + power, + report_ts, + self._last_real_reading_time, + self._current_power, + ) + self.detector.process_reading(power, now) + self._last_reading_time = now + self._last_real_reading_time = report_ts + self._current_power = power + async def _watchdog_check_stuck_cycle(self, now: datetime) -> None: """Watchdog: check if cycle is stuck (no updates for too long).""" if self.detector.state not in (STATE_RUNNING, STATE_STARTING, STATE_PAUSED, STATE_ENDING): @@ -3751,6 +4388,21 @@ class WashDataManager: if not self._last_reading_time: return + # Re-anchor the cached power on the sensor before any branch below reads it + # (#409): every decision from here on (keepalive vs force-end, low- vs + # high-power handling) is made against _current_power, and an event cache + # that has drifted from the sensor turns those decisions into fiction. + self._resync_power_from_state(now, feed_detector=True) + if self.detector.state not in ( + STATE_RUNNING, STATE_STARTING, STATE_PAUSED, STATE_ENDING + ): + # A missed reading can legitimately end the cycle (that is the point of + # picking it up). Every other injection path in this method returns + # immediately for the same reason: none of the branches below are + # meaningful once the cycle is over. + self._notify_update() + return + # Refresh the remaining-time / progress estimate on the watchdog cadence # (sampling-derived: max(30, 2*sampling+1)s), independently of incoming power # events. A publish-on-change plug emits nothing during a flat low-power tail @@ -3823,24 +4475,43 @@ class WashDataManager: return # Secondary zombie guard for unmatched cycles (expected == 0 when no profile - # has been matched). The detector hard-caps RUNNING at 8h but a chatty sensor - # that never goes silent can keep the watchdog from reaching the end state. - # Kill here after 4h so a stuck false-start doesn't run indefinitely. + # has been matched, or a hand-created profile has no learned avg_duration yet). + # This is a failsafe against a stuck FALSE START, not a cap on real programmes. + # The detector already hard-caps any cycle at 8h (cycle_detector 28800s), so + # this only ever fires *earlier*, and only when the cycle looks like it is not + # really running any more. Its old form force-ended on elapsed time alone, which + # truncated genuine long runs (issue #404: a >4h washer-dryer wash+dry drawing + # hundreds of watts was cut off at exactly 4h; the empty profile it was learning + # against was then stored force_stopped/truncated and could never learn its true + # duration, so every subsequent run repeated the deadlock). Three gates keep it + # honest without losing the false-start failsafe: + # - a device-scaled ceiling (wet/long appliances get a longer fuse), never + # above the detector's 8h absolute cap; + # - only when the appliance is effectively idle (current draw below the + # running threshold) - a cycle still pulling real power is not a stuck false + # start (reporter's evidence: the plug never dropped below 64 W); + # - skipped when an authoritative external end trigger is wired and reporting, + # since that signal will end the cycle cleanly. + # Gate on the active detector state, not _current_program: the latter is only set + # to "detecting..." on the RUNNING transition, so a cycle stuck in STARTING keeps + # a stale program and would never hit this guard. elif ( expected == 0 and not self._is_user_paused and not _verified_pause_zombie - and elapsed > 14400 - # Gate on the active detector state, not _current_program: the latter is - # only set to "detecting..." on the RUNNING transition, so a cycle stuck - # in STARTING keeps a stale program and would never hit this 4h guard. + and elapsed > self._unmatched_watchdog_ceiling + and self._current_power < self.detector.config.start_threshold_w + and not self._external_end_trigger_available() and self.detector.state in ( STATE_STARTING, STATE_RUNNING, STATE_PAUSED, STATE_ENDING ) ): self._logger.warning( - "Watchdog: Unmatched cycle exceeded 4h (%.0fs). Force-ending.", + "Watchdog: Unmatched idle cycle exceeded %.0fs (elapsed %.0fs, " + "power %.1fW). Force-ending.", + self._unmatched_watchdog_ceiling, elapsed, + self._current_power, ) self.detector.force_end(now) self._current_power = 0.0 @@ -3965,7 +4636,7 @@ class WashDataManager: time_since_real_update, self._no_update_active_timeout, ) - self.detector.process_reading(0.0, now) + self.detector.process_reading(0.0, now, synthetic=True) self._last_reading_time = now self._current_power = 0.0 self._notify_update() @@ -3980,8 +4651,11 @@ class WashDataManager: time_since_any_update ) # Ensure we handle the injection cleanly - # Do NOT update _last_real_reading_time here - self.detector.process_reading(0.0, now) + # Do NOT update _last_real_reading_time here, and tell the + # detector this reading is ours: it must still advance the + # quiet timers (that is the whole point of injecting it) but + # must not count as the sensor having reported (item 238). + self.detector.process_reading(0.0, now, synthetic=True) self._last_reading_time = now # Resets 'any' timer so we don't spam self._current_power = 0.0 self._notify_update() @@ -3998,7 +4672,7 @@ class WashDataManager: ): # Treating as start of low power wait self._logger.debug("Watchdog: Silence at low power (%.0fs). Injecting 0W.", time_since_any_update) - self.detector.process_reading(0.0, now) + self.detector.process_reading(0.0, now, synthetic=True) self._last_reading_time = now self._current_power = 0.0 self._notify_update() @@ -4017,11 +4691,35 @@ class WashDataManager: limit = (expected + 14400) if expected > 0 else 14400 # 4h default if elapsed < limit: + # Silence at high power buys the cycle more time, but it must NOT + # be turned into data (#409). This branch used to re-feed the + # cached power to the detector on every tick, which + # - wrote a reading the sensor never reported into the cycle + # trace, producing a dead-flat non-zero tail that inflates + # the stored duration and energy and skews matching; + # - reset _time_below_threshold and _last_active_time, so the + # detector's own end-of-cycle timers could never accumulate + # and the fabricated tail was self-sustaining until this + # limit expired hours later (force_stopped); + # - fed that inflated duration back into the profile once the + # cycle was labelled, growing avg_duration and therefore + # `limit`, so each subsequent stuck cycle ran longer still. + # The keepalive was never what kept the cycle alive - the + # detector only ends a cycle on quiet time it has actually + # observed - so deferring the force-end is enough. The trace + # keeps an honest hole (integrate_wh drops outage-sized + # segments), _update_remaining_only above keeps the ETA ticking + # on wall-clock, and the resync at the top of this method picks + # the real value up the moment the plug speaks again. self._logger.info( - "Watchdog: High power (%.1fW) stale (%.0fs). Injecting refresh.", - self._current_power, time_since_any_update + "Watchdog: High power (%.1fW) stale (%.0fs, sensor silent " + "%.0fs). Deferring end (elapsed %.0fs < limit %.0fs).", + self._current_power, + time_since_any_update, + time_since_real_update, + elapsed, + limit, ) - self.detector.process_reading(self._current_power, now) self._last_reading_time = now self._notify_update() return @@ -4068,6 +4766,18 @@ class WashDataManager: self._current_program = "detecting..." self._manual_program_active = False + # Confidence belongs to the match that produced it, so it cannot + # carry into the next cycle. It was only ever assigned by a real + # match update, never cleared, and the cycle-end tail stamps it onto + # cycle_data["match_confidence"] - so a cycle that never matched + # (most obviously one running a hand-pinned program, where + # _update_estimates returns early and the matcher never runs) + # persisted the PREVIOUS cycle's confidence as its own. That number + # then feeds _compute_cycle_quality_score and the learning feedback, + # i.e. fabricated match provenance on a cycle that has none - the + # #400 class of bug. Zero means "no opinion" and is not stored. + self._last_match_confidence = 0.0 + self._last_member_confidence = None self._notified_pre_completion = False self._time_remaining = None self._total_duration = None @@ -4087,6 +4797,9 @@ class WashDataManager: # Snapshot the external energy meter (issue #316) so cycle end can # take an accurate start->end delta. No-op when none is configured. self._snapshot_energy_meter_start() + # Open the dynamic price timeline (#426) with the price in force + # right now. No-op when no price entity is configured. + self._start_price_timeline() # Reset pause tracking and clean state for new cycle self._is_user_paused = False @@ -4099,6 +4812,21 @@ class WashDataManager: self._notified_clean_laundry = False self._reset_unload_nag_tracking() + # A program the user armed while idle - or pinned during STARTING, + # which the reset above would otherwise have wiped - takes effect + # here (#411). Placed after the reset for two reasons: the duration + # it sets must not be nulled a line later, and it applies the pin, + # which refreshes the estimate - so it has to run once the pause + # totals belong to THIS cycle. Reading them a few lines earlier + # computed the new cycle's first ETA from the previous cycle's + # paused seconds, which the back-to-back case makes reachable: when + # the cycle-end tail returns early on the new-cycle token guard, it + # never clears them either, and the live progress notification is + # interval-throttled, so that wrong ETA sits on the phone until the + # next allowed tick. Still before the start event, so the event + # carries the real program name rather than "detecting...". + self._consume_armed_program() + self._start_watchdog() # Start watchdog when cycle starts # Fire the start event immediately on cycle detection so listeners always @@ -4277,9 +5005,18 @@ class WashDataManager: # detector into a fresh RUNNING before post-processing completes). See B1 in # _async_process_cycle_end. end_token = self._ranking_snapshot_cycle_id + # Freeze the price timeline alongside the token, for the same reason: the + # back-to-back start that changes the token also calls _start_price_timeline, + # which replaces this cycle's timeline with the NEW cycle's opening sample + # before the task reaches the costing step (#426). + end_price_timeline = list(self._price_timeline) if not self._is_shutdown: self._cycle_end_task = self._spawn_tracked( - self._async_process_cycle_end(cycle_data, cycle_token=end_token) + self._async_process_cycle_end( + cycle_data, + cycle_token=end_token, + price_timeline=end_price_timeline, + ) ) def _ml_end_confidence( @@ -4568,6 +5305,57 @@ class WashDataManager: except Exception: # noqa: BLE001 - never break cycle storage pass + def _price_entity_reject_reason(self, entity_id: str) -> str | None: + """Why ``entity_id`` cannot be a price per kWh, or None (#439). + + Only *positive* evidence rejects: an entity that has not loaded yet carries + no attributes, and refusing it would silence a perfectly good tariff sensor + that HA sets up after us. + """ + options = self.config_entry.options + if entity_id == self.power_sensor_entity_id: + return "it is this device's power sensor" + if entity_id == options.get(CONF_ENERGY_SENSOR): + return "it is this device's energy meter" + state = self.hass.states.get(entity_id) + if state is None: + return None + device_class = str(state.attributes.get("device_class") or "").strip().lower() + if device_class in _NON_PRICE_DEVICE_CLASSES: + return f"its device class is '{device_class}'" + unit = str(state.attributes.get("unit_of_measurement") or "").strip().lower() + if unit in _NON_PRICE_UNITS: + return f"its unit is '{unit}'" + return None + + def _price_entity_id(self) -> str | None: + """The configured price entity, or None when it is provably not a price. + + Guards the single trap the cost feature has (#439): the panel picker lists + every sensor, a price entity outranks the static price, and pointing it at + the plug's own kWh counter silently charges every cycle + ``energy * meter_reading`` instead of ``energy * tariff``. Rejecting it here + - rather than in the panel alone - also repairs entries that are already + misconfigured, which fall back to the static price. + """ + entity_id = self.config_entry.options.get(CONF_ENERGY_PRICE_ENTITY) + if not entity_id: + return None + reason = self._price_entity_reject_reason(entity_id) + if reason is None: + self._warned_price_entity = None + return entity_id + if self._warned_price_entity != entity_id: + self._warned_price_entity = entity_id + self._logger.warning( + "Energy price entity %s is not a price per kWh (%s); ignoring it and " + "using the static energy price instead. Set a tariff sensor there, or " + "clear the field to cost cycles at the static price", + entity_id, + reason, + ) + return None + def _resolve_energy_price(self) -> float | None: """Current energy price per kWh, or None when none is configured. @@ -4575,22 +5363,202 @@ class WashDataManager: value. Used to freeze each cycle's cost at completion time. """ options = self.config_entry.options - price_entity = options.get(CONF_ENERGY_PRICE_ENTITY) + price_entity = self._price_entity_id() if price_entity: state = self.hass.states.get(price_entity) if state is not None: try: - return float(state.state) + value = float(state.state) except (ValueError, TypeError): pass + else: + # A non-finite reading is treated as no reading, exactly like an + # unparseable one: returning it would freeze an infinite cost + # onto the cycle (register item 211). + if math.isfinite(value): + return value static = options.get(CONF_ENERGY_PRICE_STATIC) if static is not None: try: - return float(static) + value = float(static) except (ValueError, TypeError): pass + else: + if math.isfinite(value): + return value return None + async def _async_price_history( + self, start: datetime, end: datetime + ) -> list[tuple[float, float]]: + """``(unix_ts, price)`` rows for the price entity over a window, via the + recorder (#426). + + Used to recover price changes the live listener could not see - the period + HA was down mid-cycle, a cycle that predates the feature, a device whose + history was imported from raw recorder data. Returns ``[]`` on any failure + (recorder disabled, entity excluded from recording, data purged) so the + caller falls back to whatever it already had. + """ + entity_id = self._price_entity_id() + if not entity_id: + return [] + try: + from homeassistant.components.recorder import ( # noqa: PLC0415 + get_instance, + history, + ) + except Exception: # noqa: BLE001 - recorder is an optional component + return [] + + def _query() -> list[tuple[float, float]]: + res = history.state_changes_during_period( + self.hass, start, end, entity_id, include_start_time_state=True + ) + start_ts = start.timestamp() + rows: list[tuple[float, float]] = [] + for state in res.get(entity_id, []) or []: + try: + price = float(state.state) + except (ValueError, TypeError): + # unknown/unavailable: the previous price stays in force. + continue + ts = state.last_changed.timestamp() + # include_start_time_state hands back the state in force at the + # window start, whose last_changed can predate it by hours. Clamp + # so it anchors the timeline instead of sorting before the cycle. + rows.append((max(ts, start_ts), price)) + rows.sort(key=lambda item: item[0]) + return rows + + try: + return await get_instance(self.hass).async_add_executor_job(_query) + except Exception as exc: # noqa: BLE001 - cost must never break cycle end + self._logger.debug("Price history lookup failed for %s: %s", entity_id, exc) + return [] + + async def _async_apply_cycle_cost( + self, + cycle_data: dict[str, Any], + price_timeline: list[tuple[float, float]] | None = None, + ) -> None: + """Freeze ``cost`` / ``energy_price`` onto a finished cycle (#426). + + Dynamic mode integrates the stored power trace against the price timeline + recorded while the cycle ran; ``energy_price`` then carries the *effective* + price per kWh the cycle paid (cost / kWh), which is the only figure that + stays meaningful once the tariff moved. ``energy_price_mode`` says which of + the two produced the number so the panel can label it honestly. + + Falls back to the single current price whenever dynamic costing cannot + produce an answer - no price entity, the toggle off, no timeline, or a + trace with no energy in it. Never raises: a cost figure is display-only and + must not be able to lose a finished cycle. + """ + price = self._resolve_energy_price() + if self._dynamic_pricing_enabled(): + try: + if await self._async_apply_dynamic_cost( + cycle_data, price_timeline=price_timeline + ): + return + except Exception as exc: # noqa: BLE001 - never break cycle storage + self._logger.debug("Dynamic cost calculation failed: %s", exc) + if price is not None: + cycle_data["energy_price"] = price + cycle_data["energy_price_mode"] = "fixed" + cycle_data["cost"] = round( + self._cycle_report_energy_wh(cycle_data) / 1000.0 * price, 4 + ) + + async def _async_apply_dynamic_cost( + self, + cycle_data: dict[str, Any], + price_timeline: list[tuple[float, float]] | None = None, + ) -> bool: + """Cost the cycle against its price timeline. True when it succeeded. + + ``price_timeline`` is the finished cycle's own history, frozen at cycle end. + Falling back to the live ``_price_timeline`` is only correct while no new + cycle has started since. + """ + start_dt = dt_util.parse_datetime(str(cycle_data.get("start_time") or "")) + end_dt = dt_util.parse_datetime(str(cycle_data.get("end_time") or "")) + if start_dt is None or end_dt is None or end_dt <= start_dt: + return False + + timeline = list( + self._price_timeline if price_timeline is None else price_timeline + ) + # The live listener is complete whenever HA stayed up for the whole cycle. + # Consult the recorder only when it cannot have been: nothing recorded at + # all (the feature was switched on mid-cycle), or a restart gap where price + # changes would have gone unseen. + if not timeline or cycle_data.get("restart_gaps"): + recorded = await self._async_price_history(start_dt, end_dt) + if recorded: + # The recorder saw the downtime too, so it supersedes rather than + # merges - interleaving the two would double-count a change that + # both captured at slightly different timestamps. + timeline = recorded + + if not timeline: + return False + + start_ts = start_dt.timestamp() + duration = (end_dt - start_dt).total_seconds() + points = compact_price_timeline( + [(ts - start_ts, price) for ts, price in timeline], + max_points=PRICE_TIMELINE_MAX_POINTS, + decimals=PRICE_TIMELINE_PRICE_DECIMALS, + ) + # A price recorded before the trace's first sample still sets the opening + # price; clamp rather than drop it, and discard anything past the end. + points = [(max(0.0, offset), price) for offset, price in points if offset <= duration] + points = compact_price_timeline( + points, max_points=PRICE_TIMELINE_MAX_POINTS, + decimals=PRICE_TIMELINE_PRICE_DECIMALS, + ) + if not points: + return False + + result = self._cost_from_timeline(cycle_data, points) + if result is None: + return False + cost, effective_price = result + cycle_data["cost"] = round(cost, 4) + cycle_data["energy_price"] = round(effective_price, 6) + cycle_data["energy_price_mode"] = "dynamic" + cycle_data["price_timeline"] = [ + [round(offset, 1), price] for offset, price in points + ] + return True + + def _cost_from_timeline( + self, cycle_data: dict[str, Any], points: list[tuple[float, float]] + ) -> tuple[float, float] | None: + """``(cost, effective_price)`` for a cycle and its price timeline, or None. + + Pure apart from reading the cycle; the meter-vs-integrated decision is the + same ``_cycle_report_energy_wh`` every other user-facing energy figure uses, + so the cost and the kWh shown beside it are computed from one number. + """ + # decompress_power_data, not the raw list: a cycle stored before the + # offset migration still carries ISO timestamps, and recosting must work + # on exactly the cycles that are old enough to need it. + points_xy = decompress_power_data(cast(Any, cycle_data)) + if len(points_xy) < 2: + return None + timestamps = np.asarray([t for t, _ in points_xy], dtype=float) + power = np.asarray([p for _, p in points_xy], dtype=float) + return cycle_cost( + timestamps, + power, + points, + max_gap_s=energy_gap_threshold_s(timestamps), + report_wh=self._cycle_report_energy_wh(cycle_data), + ) + def _read_energy_meter(self) -> tuple[float, str] | None: """Read the configured external energy meter, normalized to Wh. @@ -4676,6 +5644,13 @@ class WashDataManager: so every save site (shutdown, periodic, pause, resume) stays consistent. """ snapshot["manual_program"] = self._manual_program_active + # Persist the chosen program name too (#404 secondary bug): the detector + # snapshot's matched_profile is wiped on the first post-restart match tick + # because _matched_profile_duration is not restored, so the name must be + # carried explicitly to re-pin the override. + snapshot["manual_program_name"] = ( + self._current_program if self._manual_program_active else None + ) snapshot["notified_start"] = self._notified_start snapshot["start_event_fired"] = self._start_event_fired snapshot["is_user_paused"] = self._is_user_paused @@ -4685,6 +5660,11 @@ class WashDataManager: snapshot["total_user_paused_seconds"] = self._total_user_paused_seconds snapshot["energy_meter_start"] = self._energy_meter_start snapshot["energy_meter_source"] = self._energy_meter_source + # Dynamic price timeline (#426): absolute (unix_ts, price) pairs, as lists + # so the JSON round-trip is lossless. + snapshot["price_timeline"] = [ + [ts, price] for ts, price in self._price_timeline + ] return snapshot @staticmethod @@ -4747,13 +5727,18 @@ class WashDataManager: return "" async def _async_process_cycle_end( - self, cycle_data: dict[str, Any], cycle_token: str | None = None + self, + cycle_data: dict[str, Any], + cycle_token: str | None = None, + price_timeline: list[tuple[float, float]] | None = None, ) -> None: """Process cycle completion asynchronously (heavy tasks). ``cycle_token`` is the ``_ranking_snapshot_cycle_id`` captured when this cycle ended. The terminal-state reset at the tail is skipped if a new cycle has started since (token changed), so back-to-back cycles are not clobbered (B1). + ``price_timeline`` is that same cycle's tariff history, frozen at the same + moment and for the same reason; None means "read the live one". """ # FINAL PROFILE MATCH: If still detecting, try one last match with complete cycle data @@ -4772,20 +5757,89 @@ class WashDataManager: program = self._current_program match_result = self._last_match_result match_confidence = self._last_match_confidence + member_confidence = self._last_member_confidence matched_profile_duration = self._matched_profile_duration + # The number EVERY label / persistence decision for this cycle gates on. + # + # A Stage-5 group win reports the group's score, i.e. the best-scoring + # SIBLING's, while the member the cycle would be labelled as is chosen + # separately by integrated energy - so the two can be different profiles + # (item 206; measured 16.7% of whole-cycle group wins, gap up to 0.157). + # A label makes the cycle evidence for that ONE member, so it has to clear + # the bar on its own blended score, not on its sibling's. Both are blended + # pipeline scores over the same trace, so they compare directly. + # + # Computed once here because three gates read it and they must agree: the + # cycle-end gate below, the post-cycle auto-label pass (via + # MatchResult.label_confidence on its own fresh match), and the learning + # handoff - whose `_maybe_request_feedback` can auto-label without ever + # asking the user, so a member that fell short of `auto_label_confidence` + # on its own score has to be queued for confirmation instead. + # + # `member_confidence` is None for every non-group match, which leaves this + # exactly equal to the `match_confidence or 0.0` passed down before. + label_confidence = float(match_confidence or 0.0) + if member_confidence is not None: + label_confidence = min(label_confidence, float(member_confidence)) + manual_program = self._manual_program_active cycle_anomaly = self._cycle_anomaly overrun_ratio = self._overrun_ratio - # If we had a runtime match, attach the profile name for persistence + # Record which program this cycle ran - but only when we actually know it. + # + # A label is not a display value. It makes the cycle *evidence* for that + # profile (the async_rebuild_envelope right after the persist below), which + # moves the profile's avg_duration / target_duration - and those are what arm + # Smart Termination and the anti-crease finalize. So a weak guess recorded as + # fact seeds the next mis-detection: #400 measured a program's target_duration + # snapping to the duration of a fragment mis-assigned to it. + # + # The bar is the learning threshold, i.e. the ladder the panel already enforces + # (unmatch < match < learning < auto-label): a match that is not confident + # enough for WashData to ASK about is not confident enough to record as fact. + # Below it the cycle stays unlabelled and the auto-label pass below gets its + # turn on the COMPLETE trace at its own (higher) threshold, which is the better + # judge - previously any live commit, or a final match scraping its 0.15 floor, + # pre-empted that path entirely. + # + # A hand-picked program bypasses the gate: it is the user's own statement, not a + # guess. It is stamped "manual" rather than "auto_match" for the same reason - + # "auto_match" means "the matcher guessed this", which is what lets bulk + # auto-labelling overwrite a label later. if ( program and program not in ("off", "detecting...", "restored...") and program in self.profile_store.get_profiles() ): - cycle_data["profile_name"] = program - cycle_data["label_source"] = "auto_match" - if match_confidence: - cycle_data["match_confidence"] = float(match_confidence) + # `label_confidence` (computed above) is the member-aware number, not + # the group's. See its definition for why a label may not rely on a + # sibling's score. + if label_confidence > 0: + # Recorded whether or not we label, so the panel can show what + # WashData suspected without the cycle claiming it as its program. + cycle_data["match_confidence"] = label_confidence + if manual_program: + cycle_data["profile_name"] = program + cycle_data["label_source"] = "manual" + elif label_confidence >= float(self._learning_confidence or 0.0): + cycle_data["profile_name"] = program + cycle_data["label_source"] = "auto_match" + else: + self._logger.info( + "Not labeling cycle as '%s': match confidence %.2f is below the " + "learning threshold %.2f, so it stays unlabelled rather than " + "reshaping that program's statistics.%s", + program, + label_confidence, + float(self._learning_confidence or 0.0), + "" + if member_confidence is None + or float(member_confidence) >= float(match_confidence or 0.0) + else ( + f" (its profile group scored {float(match_confidence or 0.0):.2f}, " + f"but that was a different member of the group)" + ), + ) # Attach extensive debug data if available (and configured) if match_result: @@ -4806,10 +5860,14 @@ class WashDataManager: res = await self.profile_store.async_match_profile( cycle_data["power_data"], cycle_data["duration"] ) - if res.best_profile and res.confidence >= self._auto_label_confidence: + # label_confidence == confidence for everything except a Stage-5 group + # win whose selected member scored below the sibling that set the group's + # score (item 206). This is the highest-stakes gate of the three: it + # labels without ever asking the user. + if res.best_profile and res.label_confidence >= self._auto_label_confidence: cycle_data["profile_name"] = res.best_profile cycle_data["label_source"] = "auto_label_post" - cycle_data["match_confidence"] = float(res.confidence) + cycle_data["match_confidence"] = float(res.label_confidence) # Top-5 from post-cycle match (may differ from live match ranking). # Sanitize: strip heavy current/sample arrays (these fields are NOT # in the fired-event exclusion set, so they must stay small to keep @@ -4820,7 +5878,7 @@ class WashDataManager: self._logger.info( "Post-cycle auto-labeled as '%s' (confidence: %.2f)", res.best_profile, - res.confidence, + res.label_confidence, ) # Back-fill confirmed label on any ranking snapshots captured during this cycle @@ -4927,15 +5985,11 @@ class WashDataManager: restart_gaps_snapshot = list(self._restart_gaps) cycle_data["restart_gaps"] = restart_gaps_snapshot - # Freeze the energy cost onto the cycle using the price in effect NOW, so - # later price changes never rewrite historical costs. Stored as a number - # (currency per the configured price's unit); absent when no price is set. - price = self._resolve_energy_price() - if price is not None: - cycle_data["energy_price"] = price - cycle_data["cost"] = round( - self._cycle_report_energy_wh(cycle_data) / 1000.0 * price, 4 - ) + # Freeze the energy cost onto the cycle. With a dynamic tariff this is the + # power trace integrated against the price in force at each moment (#426); + # otherwise the single price in effect NOW. Either way it is frozen here, so + # later price changes never rewrite historical costs. + await self._async_apply_cycle_cost(cycle_data, price_timeline=price_timeline) # Score cycle quality with the ML model before persisting so the score is # stored on the cycle record and available to the learning manager immediately. @@ -5027,7 +6081,12 @@ class WashDataManager: # Prepare cycle data for event (enrich if needed) # IMPORTANT: Exclude large fields to prevent exceeding HA's 32KB event data limit - excluded_fields = {"power_data", "debug_data", "power_trace"} + excluded_fields = { + "power_data", "debug_data", "power_trace", + # A chatty dynamic tariff can add hundreds of entries (#426); the + # cost and effective price it produced ride along instead. + "price_timeline", + } event_cycle_data = { k: v for k, v in cycle_data.items() if k not in excluded_fields } @@ -5142,7 +6201,7 @@ class WashDataManager: self.learning_manager.process_cycle_end( cycle_data, detected_profile=program, - confidence=match_confidence or 0.0, + confidence=label_confidence, predicted_duration=matched_profile_duration, match_result=match_result, ) @@ -5168,6 +6227,8 @@ class WashDataManager: # Clear all state and timers - zero everything out self._current_program = "off" self._manual_program_active = False + # A pin is for the cycle it was made for, so it does not carry over (#411). + self.clear_armed_program() self._notified_pre_completion = False self._time_remaining = None self._matched_profile_duration = None @@ -5647,6 +6708,16 @@ class WashDataManager: if self._notify_timeout_seconds > 0: variables["timeout"] = self._notify_timeout_seconds extra_vars = {**(extra_vars or {}), "timeout": self._notify_timeout_seconds} + tap_target = self._notification_tap_target() + if tap_target and message != _CLEAR_NOTIFICATION_MARKER: + # A dismiss marker is a command, not a card - it has nothing to tap. + variables["clickAction"] = tap_target + variables["url"] = tap_target + extra_vars = { + **(extra_vars or {}), + "clickAction": tap_target, + "url": tap_target, + } # Quiet hours (do-not-disturb): hold finish-type notifications that would # wake someone and deliver them at the end of the window. Live-progress ticks @@ -5787,6 +6858,18 @@ class WashDataManager: svc_data = dict(data) svc_data.update(self._mobile_service_extras(ev, notify_service)) + # #435: the companion app reads the notification icon from + # `notification_icon`, NOT from `icon` - so the configured mdi icon was + # being sent under a key no companion platform looks at. `icon` stays in + # the base payload for the platforms that do use it (notify.html5 and + # friends); the mobile-only alias is added here. The same key now covers + # both platforms: Android draws it in the status bar, and iOS renders it + # as a communication-notification avatar in place of the app icon from + # companion app 2026.8.0 (home-assistant/iOS#4672). Older iOS builds + # ignore the key rather than failing, so there is nothing to gate on. + if icon and self._is_mobile_notify_service(notify_service): + svc_data["notification_icon"] = icon + state = ( self.hass.states.get(notify_service) if notify_service.startswith("notify.") @@ -6059,6 +7142,7 @@ class WashDataManager: self._projected_cost = None self._cycle_anomaly = "none" self._overrun_ratio = 0.0 + self._envelope_position = None self._last_match_result = None self._notify_update() return @@ -6140,6 +7224,25 @@ class WashDataManager: ) return service.startswith("mobile_app") + def _notification_tap_target(self) -> str: + """Where a tap on this device's notifications should land (#438). + + Blank (the shipped default) resolves to this appliance's own panel deep link + ``/ha-washdata?device=``, so a notification about the dryer opens + the dryer instead of whichever appliance the panel happened to show last. + The entry id is used rather than the title because it survives a rename, and + it needs no URL escaping. A user-supplied value wins, and the literal + ``none`` turns the tap target off entirely. + + Emitted as both ``clickAction`` (Android) and ``url`` (iOS), which are the + two companion-app keys for the same thing; both are mobile-only and are + filtered to ``mobile_app_*`` targets by ``_mobile_service_extras``. + """ + configured = (self._notify_live_click_action or "").strip() + if configured: + return "" if configured.lower() == "none" else configured + return f"/{PANEL_URL_PATH}?device={self.entry_id}" + @property def _timer_pause_action_id(self) -> str: """Stable mobile action ID for timer-pause Resume button, unique per device.""" @@ -6158,18 +7261,28 @@ class WashDataManager: return max(1, int(np.ceil(estimated_updates * (1.0 + overrun_ratio)))) def _apply_live_notification_prefs(self, extra_vars: dict[str, Any]) -> None: - """Inject the user's opt-in live-notification data keys (#347). + """Inject the user's live-notification data keys (#347, #417). - ``sticky`` keeps the live notification on screen when tapped; ``clickAction`` - gives the notification a tap target (e.g. a dashboard path). Both are - mobile-only keys forwarded only to ``mobile_app_*`` live targets. Defaults - (sticky off, empty clickAction) add nothing, so the payload is byte-identical - to before unless the user opts in. + ``sticky`` keeps the live notification on screen when tapped. It is a + mobile-only key forwarded only to ``mobile_app_*`` live targets, and its + default (off) adds nothing, so the payload is byte-identical to before + unless the user opts in. The tap target itself is no longer applied here: + it belongs to every event type, so ``_dispatch_notification`` injects + ``_notification_tap_target()`` centrally instead (#438). + + ``silent`` (#417) marks a *refresh* of the running Live Activity as a + non-alerting, lower-priority push, which is what stops iOS playing a sound and + vibrating on every interval tick; ``push.interruption-level: passive`` is the + companion's generic quiet key and covers the case where the update is rendered + as an ordinary banner instead. Neither is applied to the update that STARTS the + activity: that one is the "cycle is now on your Lock Screen" cue and stays + audible (per the companion docs ``silent`` has no effect there in any case). """ if self._notify_live_sticky: extra_vars["sticky"] = "true" - if self._notify_live_click_action: - extra_vars["clickAction"] = self._notify_live_click_action + if self._notify_live_silent and self._live_activity_started: + extra_vars["silent"] = True + extra_vars["push"] = {"interruption-level": "passive"} def _check_live_progress_notification(self) -> None: """Send throttled live progress notifications for compatible mobile targets.""" @@ -6178,8 +7291,18 @@ class WashDataManager: if self.detector.state not in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): return + # #437: a match landing is not the same as an ETA existing. + # _update_remaining_only() is throttled to one estimate per 5 s, so the tick + # that accepts a match can find _matched_profile_duration set while + # _time_remaining is still None - and the progress branch below renders that + # `float(self._time_remaining or 0.0)` as remaining 0, i.e. elapsed == total + # (a full 100 % bar) and minutes_left == 1 ("less than 1 minute") at the very + # start of the cycle. Require a real estimate; the waiting latch keeps that + # tick silent rather than re-sending the waiting message. has_profile_match = bool( - self._matched_profile_duration and self._matched_profile_duration > 0 + self._matched_profile_duration + and self._matched_profile_duration > 0 + and self._time_remaining is not None ) if has_profile_match: # A profile has been matched - reset the waiting latch so future @@ -6601,6 +7724,7 @@ class WashDataManager: self._projected_energy_wh = None self._projected_cost = None return + live_cost = self._live_cost_so_far(trace) wh, cost = progress_mod.projected_energy( self.profile_store, self.config_entry.options, @@ -6612,10 +7736,48 @@ class WashDataManager: price, self._profile_end_expectation, self._logger, + cost_so_far=live_cost[0] if live_cost else None, + cost_so_far_wh=live_cost[1] if live_cost else None, ) self._projected_energy_wh = wh self._projected_cost = cost + def _live_cost_so_far( + self, trace: list[tuple[datetime, float]] + ) -> tuple[float, float] | None: + """``(cost, charged_wh)`` incurred so far at the prices the cycle ran through. + + Returns ``None`` when dynamic pricing is off or nothing has been recorded + yet, which puts :func:`progress.projected_energy` back on the flat-price + formula. The second element is the energy that cost was charged for, which + the projection must subtract instead of ``energy_so_far``: this integrates + the trace, while ``energy_so_far`` is the detector's per-reading + accumulator, and the two treat outages and sub-threshold intervals + differently. Never raises. + """ + if not self._dynamic_pricing_enabled() or not self._price_timeline: + return None + try: + if len(trace) < 2 or self._cycle_start_time is None: + return None + start_ts = self._cycle_start_time.timestamp() + timestamps = np.asarray([t.timestamp() - start_ts for t, _ in trace], dtype=float) + power = np.asarray([p for _, p in trace], dtype=float) + points = compact_price_timeline( + [(ts - start_ts, price) for ts, price in self._price_timeline], + max_points=PRICE_TIMELINE_MAX_POINTS, + decimals=PRICE_TIMELINE_PRICE_DECIMALS, + ) + max_gap_s = energy_gap_threshold_s(timestamps) + result = cycle_cost(timestamps, power, points, max_gap_s=max_gap_s) + if result is None: + return None + # integrate_wh is the same total the price segments sum to, by the + # documented contract of integrate_wh_by_price. + return result[0], float(integrate_wh(timestamps, power, max_gap_s=max_gap_s)) + except Exception: # noqa: BLE001 - projection must never break estimates + return None + def _update_cycle_anomaly(self, duration_so_far: float) -> None: """Flag a *soft* runtime overrun anomaly for the running cycle. @@ -6641,12 +7803,25 @@ class WashDataManager: self._projected_cost = None self._cycle_anomaly = "none" self._overrun_ratio = 0.0 + self._envelope_position = None return now = dt_util.now() + # The 5 s throttle guards the heavy phase estimate, but the FIRST estimate + # after a match must not wait it out (#437): the match callback calls this + # and then _check_live_progress_notification(), which now stays in the + # waiting branch while _time_remaining is None. Bypassing once per match + # means the live notification switches to a real countdown immediately + # instead of holding the waiting message for another interval. + first_estimate_after_match = ( + self._time_remaining is None + and bool(self._matched_profile_duration) + and self._matched_profile_duration > 0 + ) if ( self._last_phase_estimate_time and (now - self._last_phase_estimate_time).total_seconds() < 5.0 + and not first_estimate_after_match ): return self._last_phase_estimate_time = now @@ -6666,6 +7841,7 @@ class WashDataManager: self._projected_cost = None self._cycle_anomaly = "none" self._overrun_ratio = 0.0 + self._envelope_position = None self._logger.debug( "No profile matched yet, elapsed=%smin", int(duration_so_far / 60) ) @@ -6986,6 +8162,18 @@ class WashDataManager: """Elapsed / expected duration for the running cycle (0.0 when unknown).""" return self._overrun_ratio + @property + def envelope_position(self) -> float | None: + """How far this run has mapped onto its profile's envelope, 0-1, or None. + + Produced by the DTW alignment that runs for the verified-pause decision, + so it is only refreshed while power is below the stop threshold and a + profile is matched - which is exactly the phase where elapsed time says + least (a dishwasher sitting in its drying phase). Visible only; no + detection path reads it. + """ + return self._envelope_position + @property def last_cycle_post_anomaly(self) -> dict: """Post-cycle anomaly data from the last completed cycle. @@ -7018,8 +8206,15 @@ class WashDataManager: @property def current_power(self): - """Return current power reading in watts.""" - return self._current_power + """Return current power reading in watts. + + Prefers the sensor's live state over the event cache (#409): the cache is + only refreshed while a cycle is active or expiring, so an idle appliance + whose plug reports rarely would otherwise show whatever value was last seen + - which is what users compared against their HA sensor and found wrong. + """ + live = self._live_power_state() + return live[0] if live is not None else self._current_power @property def cycle_start_time(self) -> datetime | None: @@ -7095,15 +8290,34 @@ class WashDataManager: """Lifetime accumulated energy (kWh) for the HA Energy dashboard sensor.""" return round(self.profile_store.get_lifetime_energy_wh() / 1000.0, 3) + @property + def lifetime_cycle_count(self) -> int: + """Cycles this appliance has run, ever - the odometer behind the count sensor. + + Distinct from :attr:`cycle_count`, which is ``len(retained history)`` and is + the right basis for "do I have enough data yet" gates. This one only ever + rises: it survives retention trimming, record deletion and a data wipe, so an + "every N cycles" maintenance schedule (ours or an external integration's) can + be built on it (#414). + """ + return self._lifetime_cycle_count() + @property def manual_program_active(self) -> bool: """Return True if a manual program override is active.""" return getattr(self, "_manual_program_active", False) - def set_manual_program(self, profile_name: str) -> None: - """Manually set the current program.""" - if self.detector.state != "running": - return + @property + def armed_program(self) -> str | None: + """The program pinned for the next cycle, when one is not under way (#411).""" + return getattr(self, "_armed_program", None) + + def _resolve_profiles(self) -> dict[str, Any]: + """Return the stored profiles mapping, falling back to the raw store dict. + + Shared by the manual-program set and re-arm paths so both agree on what + counts as an existing program. Never raises. + """ profiles_raw: Any = None try: profiles_raw = self.profile_store.get_profiles() @@ -7111,37 +8325,195 @@ class WashDataManager: profiles_raw = None if isinstance(profiles_raw, dict): - profiles: dict[str, Any] = cast(dict[str, Any], profiles_raw) - else: - profiles_fallback = getattr(self.profile_store, "_data", {}).get( - "profiles", {} - ) - profiles = ( - cast(dict[str, Any], profiles_fallback) - if isinstance(profiles_fallback, dict) - else {} - ) + return cast(dict[str, Any], profiles_raw) + profiles_fallback = getattr(self.profile_store, "_data", {}).get("profiles", {}) + return ( + cast(dict[str, Any], profiles_fallback) + if isinstance(profiles_fallback, dict) + else {} + ) + + def set_manual_program(self, profile_name: str) -> bool: + """Pin a program to the current cycle, or arm it for the next one (#411). + + Returns True when the choice was accepted. It used to return silently + unless the detector was exactly in ``running``, which meant selecting a + program on an idle appliance (the overwhelmingly common case: the panel + offers the dropdown at all times) did nothing at all, reported success to + the caller because there was no return value to test, and logged nothing. + The selection then snapped back to auto-detect on the next refresh. + + Now every state is accepted. While a cycle is under way the program is + applied to it immediately; otherwise it is armed and applied the moment + the next cycle starts, which is what someone picking a program on an idle + machine means by it. The only rejection left is a program that does not + exist, and that one is reported rather than swallowed. + """ + profiles = self._resolve_profiles() if profile_name not in profiles: self._logger.warning("Cannot set manual program: '%s' not found", profile_name) - return + return False + in_progress = self.detector.state in _CYCLE_IN_PROGRESS_STATES + # The arm survives only where the next cycle still needs it. Applied to a + # cycle already under way the pin belongs to THAT cycle, and leaving it armed + # let a back-to-back load inherit it: the cycle-end tail does try to clear it + # ("a pin is for the cycle it was made for") but sits behind the new-cycle + # token guard and returns early in exactly that case, so _consume_armed_program + # would stamp an unrelated cycle `label_source = "manual"` and let it reshape + # that program's envelope. + # + # STARTING is the one in-progress state that must KEEP the arm: the + # STARTING -> RUNNING transition resets the live pin as it starts the new + # cycle, and _consume_armed_program is what puts it back. + keep_arm = (not in_progress) or self.detector.state == STATE_STARTING + armed = profile_name if keep_arm else None + # Resolved before persisting, so this is ONE store write. Setting and then + # clearing scheduled two async_set_armed_program tasks and two saves for + # every mid-cycle pin. + self._armed_program = armed + self._persist_armed_program(armed) + + if in_progress: + self._apply_manual_program(profile_name, profiles.get(profile_name)) + else: + self._logger.info( + "Program %r armed; it will be applied when the next cycle starts", + profile_name, + ) + return True + + @staticmethod + def _profile_duration(value: Any) -> float | None: + """A profile's expected duration in seconds, or None when unusable. + + None is the field's declared "unknown" and every reader of + ``_matched_profile_duration`` already guards for it. Non-finite is rejected + for the same reason ``_finite_power`` rejects it: ``inf`` survives a plain + ``> 0`` test, and ``sensor.py``'s ``int(time_remaining / 60)`` then raises + OverflowError on every update. A profile written by this device is always + finite; an imported or hand-edited one need not be (register items 211/229). + + OverflowError is caught alongside the rest because ``json`` keeps an + oversized integer literal as an unbounded ``int``: ``float(10**400)`` + raises instead of returning ``inf``, so the non-finite filter below is + never reached and the raise escapes a ``@callback`` WS handler. ``1e400`` + parses to ``inf`` and is the case the filter covers; the two are different + inputs (register items 279/280). + """ + try: + avg = float(value) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(avg) or avg <= 0: + return None + return avg + + def _apply_manual_program( + self, profile_name: str, profile: dict[str, Any] | None + ) -> None: + """Pin *profile_name* to the cycle in progress. Shared by set and re-arm.""" self._current_program = profile_name self._manual_program_active = True - # Update expected duration immediately - profile = profiles.get(profile_name) - if profile: - avg = float(profile.get("avg_duration", 0.0)) - if avg > 0: - self._matched_profile_duration = avg - self._logger.info( - "Manual program set to %s, duration=%.0fs", profile_name, avg - ) + # Update expected duration immediately. A profile with nothing learned yet + # (hand-created, or imported before its first cycle) must CLEAR the duration + # rather than leave the previously matched program's behind: the pin is + # applied mid-cycle, so the ETA and progress would go on describing the + # program the user just replaced. None is this field's established "unknown" + # value and every reader guards for it. Same shape as the restart path that + # re-pins a manual program (see the #404 secondary-bug block above), which + # already got this right. + avg = self._profile_duration(profile.get("avg_duration")) if profile else None + self._matched_profile_duration = avg + if avg: + self._logger.info( + "Manual program set to %s, duration=%.0fs", profile_name, avg + ) + else: + self._logger.info( + "Manual program set to %s; it has no learned duration yet, so the " + "time estimate stays unknown until it does", + profile_name, + ) - # Update estimates if running - if self.detector.state == "running": - self._update_estimates() + # Refresh whatever estimate the current state exposes, then publish. Runs for + # the cleared case too, so a stale remaining time is not left on display until + # the next tick. + # + # Every live state, not just RUNNING: set_manual_program applies the pin in + # PAUSED and ENDING as well (_CYCLE_IN_PROGRESS_STATES), and neither + # select.py nor _update_remaining_only publishes on its own, so on the + # select-entity path nothing reached the sensors at all. The phase + # estimator's 5 s throttle is bypassed because this is a user action, not a + # tick, and the value it invalidates is on screen right now. + # + # STARTING is deliberately excluded from the refresh: it exposes no estimate, + # and _update_estimates() treats it as a dead state - it would reset + # _current_program to "off" and undo the pin we just applied. + state = self.detector.state + if state in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): + self._last_phase_estimate_time = None + if state == STATE_RUNNING: + self._update_estimates() + else: + self._update_remaining_only() + self._notify_update() + + def _persist_armed_program(self, profile_name: str | None) -> None: + """Persist the armed program without blocking the caller. Never raises.""" + try: + # Tracked, not bare async_create_task: this writes to the ProfileStore + # and saves, so a reload/unload that swaps the store out mid-flight must + # be able to cancel it rather than let it write to the stale one. That is + # the documented rule for every store-touching fire-and-forget in this + # class; this call site was the one that did not follow it. + self._spawn_tracked( + self.profile_store.async_set_armed_program(profile_name) + ) + except Exception: # pylint: disable=broad-exception-caught + self._logger.debug("Could not persist the armed program", exc_info=True) + + def clear_armed_program(self) -> bool: + """Drop any program armed for the next cycle. True if one was armed. + + `_armed_program` is the authoritative copy; the store key is only there so + an arm survives a restart. So every site that retires an arm has to clear + BOTH, and there were three inline copies of this pair plus one place that + cleared only the store - the wipe (`clear_all_data` pops `armed_program`, + `ws_wipe_history` never touched the field), which left a pre-wipe pin ready + to be re-applied as soon as a profile of the same name existed again. + """ + if self._armed_program is None: + return False + self._armed_program = None + self._persist_armed_program(None) + return True + + def _consume_armed_program(self) -> bool: + """Apply an armed program to the cycle that just started, if there is one. + + Called from the new-cycle reset, which is also what would otherwise wipe a + program pinned during STARTING. Returns True when one was applied, so the + caller knows not to fall back to "detecting...". + """ + name = self._armed_program + if not name: + return False + profiles = self._resolve_profiles() + if name not in profiles: + # Deleted between arming and starting: drop it rather than pinning a + # program that no longer exists. + self._logger.info( + "Armed program %r no longer exists; reverting to auto-detect", name + ) + self.clear_armed_program() + return False + self._apply_manual_program(name, profiles.get(name)) + self.clear_armed_program() + self._logger.info("Applied armed program %r to the cycle just started", name) + return True async def async_pause_cycle(self) -> bool: """Pause the current cycle (user-triggered). @@ -7306,8 +8678,21 @@ class WashDataManager: self._notify_update() def clear_manual_program(self) -> None: - """Clear manual program override.""" + """Clear the manual program override, live or merely armed (#411). + + Previously bailed out unless a pin was active on a running cycle, which + left an armed program stuck: picking "Auto-detect" on an idle appliance + could not undo a choice made a moment earlier. + """ + had_arm = self._armed_program is not None + if had_arm: + self._armed_program = None + self._persist_armed_program(None) + if not self._manual_program_active: + if had_arm: + self._notify_update() + self._logger.info("Armed program cleared, reverting to auto-detection") return self._manual_program_active = False diff --git a/custom_components/ha_washdata/manifest.json b/custom_components/ha_washdata/manifest.json index b7119d15..b5efbfa0 100644 --- a/custom_components/ha_washdata/manifest.json +++ b/custom_components/ha_washdata/manifest.json @@ -21,5 +21,5 @@ "requirements": [ "numpy" ], - "version": "0.5.5" + "version": "0.5.6" } \ No newline at end of file diff --git a/custom_components/ha_washdata/playground.py b/custom_components/ha_washdata/playground.py index 508f4898..b0ef0acf 100644 --- a/custom_components/ha_washdata/playground.py +++ b/custom_components/ha_washdata/playground.py @@ -49,12 +49,21 @@ from . import analysis from . import notification_rules as notif_rules from . import progress as progress_mod from .phase_segmenter import phase_matching_enabled +from .signal_processing import ( + resample_adaptive, + compact_price_timeline, + cycle_cost, + energy_gap_threshold_s, + integrate_wh, +) from .const import ( CONF_ANTI_WRINKLE_ENABLED, CONF_ANTI_WRINKLE_EXIT_POWER, CONF_ANTI_WRINKLE_IDLE_TIMEOUT, CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE, CONF_SMART_TERMINATION_DURATION_RATIO, + CONF_ANTI_CREASE_FINALIZE_RATIO, + CONF_CURVE_PREROLL_SECONDS, CONF_ANTI_WRINKLE_MAX_DURATION, CONF_ANTI_WRINKLE_MAX_POWER, CONF_COMPLETION_MIN_SECONDS, @@ -97,6 +106,7 @@ from .const import ( MATCH_MAE_PEAK_FLOOR, MATCH_MAE_REF_PEAK, MATCH_MAE_SCALE, + MATCH_MIN_RESAMPLED_POINTS, PLAYGROUND_STRESS_DENSE_DURATION_S, PLAYGROUND_STRESS_DENSE_STEP_S, PLAYGROUND_STRESS_FLOOR_PERCENTILE, @@ -115,10 +125,16 @@ from .const import ( STATE_UNKNOWN, TerminationReason, ) -from .cycle_detector import CycleDetector, CycleDetectorConfig +from .cycle_detector import ( + CycleDetector, + CycleDetectorConfig, + effective_anticrease_finalize_ratio, + effective_curve_preroll_seconds, +) from .profile_store import ( _ambiguity_from_candidates, _match_prefix_ambiguity, + collapse_group_candidates, decompress_power_data, ) @@ -174,6 +190,8 @@ _OVERRIDE_FIELD_MAP: dict[str, tuple[str, Callable[[Any], Any]]] = { CONF_ANTI_WRINKLE_IDLE_TIMEOUT: ("anti_wrinkle_idle_timeout", float), CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE: ("dishwasher_end_spike_quiet_release", float), CONF_SMART_TERMINATION_DURATION_RATIO: ("smart_termination_duration_ratio", float), + CONF_ANTI_CREASE_FINALIZE_RATIO: ("anti_crease_finalize_ratio", float), + CONF_CURVE_PREROLL_SECONDS: ("curve_preroll_seconds", float), CONF_OFF_DELAY: ("off_delay", int), CONF_MIN_OFF_GAP: ("min_off_gap", int), CONF_COMPLETION_MIN_SECONDS: ("completion_min_seconds", int), @@ -302,7 +320,11 @@ def sanitize_setting_values(values: Any) -> dict[str, Any]: _target, coerce = mapping try: coerced = coerce(value) - except (TypeError, ValueError): + # OverflowError: the preset payload is JSON-decoded, so an oversized + # integer literal arrives as an unbounded int and float() on one raises + # rather than returning inf. Dropping the value is this function's + # documented behaviour; escaping would fail the whole save. + except (TypeError, ValueError, OverflowError): continue if isinstance(coerced, float) and not math.isfinite(coerced): continue @@ -404,15 +426,19 @@ def _build_match_snapshots( its sample cycle's decompressed trace, plus the store's live matching config (with any on-device tuned weight overrides merged in). - Also applies Stage-5 group collapsing via - :meth:`ProfileStore._grouped_snapshots`: returns the collapsed snapshot list - (where each cohesive group is represented by a single ``__group__*`` - aggregate candidate), plus ``group_members`` and ``member_snaps`` for the - Stage-5 member-resolution step in the faithful history runner. + Also resolves Stage-5 groups via :meth:`ProfileStore._grouped_snapshots`, the + same call the live matcher makes. Note what that returns since #400: the + **individual member** snapshots, unchanged, plus ``group_members`` and + ``member_snaps``. It no longer averages a family into one ``__group__*`` + aggregate - that averaged curve belonged to no member and cost the family its + program-level match, so members are scored individually and each cohesive + family is collapsed to its best member afterwards by + :func:`collapse_group_candidates`. This docstring described the old aggregate + behaviour long after the code stopped doing it. - Returns ``(grouped_snapshots, match_config, group_members, member_snaps)``. - When no cohesive groups exist ``group_members`` and ``member_snaps`` are both - empty dicts and behaviour is identical to before. + Returns ``(snapshots, match_config, group_members, member_snaps)``. When no + cohesive groups exist ``group_members`` and ``member_snaps`` are both empty + dicts and behaviour is identical to before. """ snapshots: list[dict[str, Any]] = [] try: @@ -485,11 +511,13 @@ def _build_match_snapshots( _LOGGER.debug("Playground: _grouped_snapshots failed: %s", exc) grouped_snaps = snapshots - config = _matching_config(store) + # in_progress: the sim replays a cycle step by step, so every match it runs is + # a live one - the same footing as manager._async_do_perform_matching (#400). + config = _matching_config(store, in_progress=True) return grouped_snaps, config, group_members, member_snaps -def _matching_config(store: Any) -> dict[str, Any]: +def _matching_config(store: Any, in_progress: bool = False) -> dict[str, Any]: """Live matcher config from the store (defaults + tuned overrides).""" config: dict[str, Any] = { "min_duration_ratio": float(getattr(store, "_min_duration_ratio", 0.07)), @@ -497,6 +525,7 @@ def _matching_config(store: Any) -> dict[str, Any]: "dtw_bandwidth": float(getattr(store, "dtw_bandwidth", 0.2)), # Mirror the live Stage-4 energy discriminator so the sim is byte-identical. "energy_mode": str(getattr(store, "energy_mode", "mean")), + "in_progress": bool(in_progress), } try: overrides = store._matching_overrides() # pylint: disable=protected-access @@ -728,6 +757,17 @@ class _DetailSim: self.store = store self.options = options or {} self.price = price + # Dynamic tariff timeline frozen onto the stored cycle (#426). Replaying it + # is what keeps the sim's projected cost identical to the one the live + # estimator produced for that cycle; without it a dynamically-priced cycle + # would replay at a single flat price and silently diverge. + self.price_points = compact_price_timeline( + [ + (entry[0], entry[1]) + for entry in (cycle.get("price_timeline") or []) + if isinstance(entry, (list, tuple)) and len(entry) >= 2 + ] + ) self.compute_series = compute_series self.config = build_sim_config(base_config, settings_override) self.device_type = _device_type_of(self.config) @@ -883,8 +923,40 @@ class _DetailSim: def _matcher(self, det_readings: list[tuple[datetime, float]]): if len(det_readings) < 5 or not self.snapshots: return (None, 0.0, 0.0, None, False, False) - powers = [p for _, p in det_readings] duration = (det_readings[-1][0] - det_readings[0][0]).total_seconds() + # Resample onto a uniform TIME grid exactly as async_match_profile does + # before calling the worker. Without this the sim fed the matcher a + # sample-weighted series: on a change-based plug a quiet stretch emits + # almost no rows, so its mean power was biased toward the busy part of the + # cycle while every candidate curve is uniform in time. That divergence + # only showed up once Stage 4 started comparing like with like (#400) - it + # flipped a dishwasher onto its hotter sibling in the sim while production, + # which resamples, kept the right one. + # The three ways production declines to match are reproduced exactly, because + # a sim that matches where production returns nothing is worse than useless + # for the question the Playground exists to answer. async_match_profile + # returns an empty MatchResult when resampling yields no segment, when the + # longest segment is under MATCH_MIN_RESAMPLED_POINTS, and when preprocessing + # raises; falling back to the raw series here instead let the sim match a + # 5-point stretch that production rejects (the detector calls this from 5 + # readings up). + try: + t0 = det_readings[0][0].timestamp() + segments, _used_dt = resample_adaptive( + np.array([r[0].timestamp() - t0 for r in det_readings]), + np.array([float(r[1]) for r in det_readings]), + min_dt=5.0, + gap_s=21600.0, + ) + if not segments: + return (None, 0.0, 0.0, None, False, False) + current_seg = max(segments, key=lambda s: len(s.power)) + if len(current_seg.power) < MATCH_MIN_RESAMPLED_POINTS: + return (None, 0.0, 0.0, None, False, False) + powers = current_seg.power.tolist() + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.debug("Playground detail resample failed", exc_info=True) + return (None, 0.0, 0.0, None, False, False) try: candidates = analysis.compute_matches_worker( powers, duration, self.snapshots, self.match_config @@ -899,14 +971,25 @@ class _DetailSim: # Hold any committed match on a transient miss (as the manager does). self.last_match.update(ambiguous=False) return (None, 0.0, 0.0, None, False, False) + # Mirror of async_match_profile: members are scored individually, then each + # cohesive family is collapsed to its best member before anything reads the + # ranking (#400). + candidates = collapse_group_candidates(candidates, self.group_members or {}) + # Stage-5 safeguards #2 and #3, captured here and applied after the + # top-level ambiguity call below so the ORDER matches async_match_profile. + stage5_member_fit: float | None = None + stage5_group_win = False if self.group_members and candidates[0].get("name", "").startswith("__group__"): gkey = candidates[0]["name"] members = self.group_members.get(gkey, []) if members and self.store is not None: try: - member_name, _, member_dur = self.store._stage5_pick_member( # noqa: SLF001 - list(powers), duration, members, self.member_snaps or {} + member_name, member_fit, member_dur = self.store._stage5_pick_member( # noqa: SLF001 + list(powers), duration, members, self.member_snaps or {}, + in_progress=bool(self.match_config.get("in_progress")), ) + stage5_member_fit = member_fit + stage5_group_win = True # Carry the member's duration as well, exactly as # `async_match_profile` relabels the winner: leaving the group's # aggregate duration here fed the wrong expected value to the @@ -919,6 +1002,22 @@ class _DetailSim: pass best = candidates[0] margin, is_ambiguous = _ambiguity_from_candidates(candidates) + if stage5_group_win: + # Safeguard #2: the family matched, but the chosen member does not + # individually fit near the group score, so the real program may be a + # different single profile. Same 0.55x coarse backstop as production. + _bscore = float(best.get("score") or 0.0) + if stage5_member_fit is not None and _bscore > 0 and stage5_member_fit < 0.55 * _bscore: + is_ambiguous = True + # Safeguard #3 (overrun): already past the chosen member's expected + # duration, so this may be the LONGER member of the family. + _bdur = float(best.get("profile_duration") or 0.0) + if _bdur and duration > _bdur * 1.05: + is_ambiguous = True + # Without these two the sim reports a confident match exactly where + # production downgrades to uncertain, and `is_ambiguous` is what blocks + # Smart Termination - so the replay would finalise where the real + # detector falls through to the power timeout. raw_name = best.get("name") raw_conf = float(best.get("score") or 0.0) raw_expected = float(best.get("profile_duration") or 0.0) @@ -963,11 +1062,22 @@ class _DetailSim: # bubble through _try_profile_match, which drops the match at debug - so EVERY # match in the sim would be silently reported as unmatched. tail_power = None + terminal_high = None if self.store is not None and raw_name: try: tail_power = self.store.profile_tail_power(raw_name) except Exception: # pylint: disable=broad-exception-caught tail_power = None + # Element 10 (#399), guarded the same way: the anti-crease spin guard is + # only meaningful for a device that runs anti-wrinkle at all. + det = getattr(self, "detector", None) + if det is not None and det.config.anti_wrinkle_enabled: + try: + terminal_high = self.store.profile_terminal_high_block( + raw_name, det.config.anti_wrinkle_max_power + ) + except Exception: # pylint: disable=broad-exception-caught + terminal_high = None return ( raw_name, raw_conf, @@ -978,8 +1088,49 @@ class _DetailSim: bool(full_shape_hit or prefix_fit_hit), bool(full_shape_hit), tail_power, + terminal_high, ) + def _price_at(self, offset_s: float) -> float | None: + """The tariff in force at a replay offset, or the flat price with no timeline. + + Live charges the *remaining* energy at whatever ``_resolve_energy_price()`` + returns at that instant, so a replay has to move through its own stored + timeline instead of pinning the whole cycle to one price. Otherwise a + dynamically-priced cycle diverges from the projection the live estimator + actually produced, which is the one thing this sim exists to reproduce. + """ + price = self.price + for point_offset, point_price in self.price_points: + if point_offset > offset_s: + break + price = point_price + return price + + def _cost_so_far( + self, trace: list[tuple[datetime, float]] + ) -> tuple[float, float] | None: + """``(cost, charged_wh)`` incurred up to this point of the replay, or None. + + Mirrors ``manager._live_cost_so_far``: the integrated trace charged at the + prices the cycle actually ran through. None (no stored timeline) puts the + projection back on the flat-price formula, which is the right answer for a + cycle recorded before dynamic pricing existed. + """ + if not self.price_points or len(trace) < 2: + return None + try: + base_ts = self.base.timestamp() + timestamps = np.asarray([t.timestamp() - base_ts for t, _ in trace], dtype=float) + power = np.asarray([p for _, p in trace], dtype=float) + max_gap_s = energy_gap_threshold_s(timestamps) + result = cycle_cost(timestamps, power, self.price_points, max_gap_s=max_gap_s) + if result is None: + return None + return result[0], float(integrate_wh(timestamps, power, max_gap_s=max_gap_s)) + except Exception: # noqa: BLE001 - the sim never raises + return None + def _sample(self, ts: datetime) -> None: if not self.compute_series: return # batch/sweep rows only need the outcome, not the per-step series @@ -1041,9 +1192,12 @@ class _DetailSim: pt["phase"] = progress_mod.current_phase( self.store, state, program, result.progress ) + sim_cost = self._cost_so_far(trace) wh, cost = progress_mod.projected_energy( self.store, self.options, matched_dur, trace, program, result.progress, - energy_wh, self.price, self._end_exp_fn, + energy_wh, self._price_at(offset), self._end_exp_fn, + cost_so_far=sim_cost[0] if sim_cost else None, + cost_so_far_wh=sim_cost[1] if sim_cost else None, ) pt["projected_energy_wh"] = round(wh, 1) if wh is not None else None pt["projected_cost"] = round(cost, 4) if cost is not None else None @@ -1423,6 +1577,15 @@ def _sim_config_summary(config: CycleDetectorConfig) -> dict[str, Any]: "anti_wrinkle_idle_timeout": getattr(config, "anti_wrinkle_idle_timeout", None), "dishwasher_end_spike_quiet_release": getattr(config, "dishwasher_end_spike_quiet_release", None), "smart_termination_duration_ratio": getattr(config, "smart_termination_duration_ratio", None), + # Both are clamped by the detector wherever it reads them, so report the + # effective figure: a summary carrying an out-of-range override would + # describe a sim that did not run. + "anti_crease_finalize_ratio": effective_anticrease_finalize_ratio( + getattr(config, "anti_crease_finalize_ratio", None) + ), + "curve_preroll_seconds": effective_curve_preroll_seconds( + getattr(config, "curve_preroll_seconds", None) + ), } diff --git a/custom_components/ha_washdata/profile_store.py b/custom_components/ha_washdata/profile_store.py index d2f84d80..0bf91062 100644 --- a/custom_components/ha_washdata/profile_store.py +++ b/custom_components/ha_washdata/profile_store.py @@ -26,6 +26,7 @@ import logging import math import os import re +import statistics import threading import uuid from datetime import datetime, timedelta @@ -41,9 +42,13 @@ from .const import ( CLUSTER_RESAMPLE_N, CLUSTER_SHAPE_SIMILARITY_THRESHOLD, GROUP_MIN_COHESION, + TERMINAL_EVENT_PEAK_FRAC, + TERMINAL_QUIET_MIN_S, + TERMINAL_SIGNATURE_MIN_CYCLES, MAINTENANCE_EVENT_TYPES, MAINTENANCE_RECENT_SUPPRESS_DAYS, MATCH_AMBIGUITY_MARGIN, + MATCH_MIN_RESAMPLED_POINTS, PHASE_CONSISTENCY_MIN_CYCLES, PHASE_PROFILE_MIN_CYCLES, PHASE_HEAT_CV_WARN, @@ -369,6 +374,39 @@ class MatchResult: # NOT for the anti-crease finalize, where blocking can re-hang a cycle the way # #296 described. That consumer reads this narrower flag instead. is_prefix_ambiguous_full_shape: bool = False + # Stage 5 only (None for every non-group match): the blended pipeline score + # the SELECTED member earned on its own curve. `confidence` deliberately stays + # the group's score - the best-scoring sibling's - because that is what the + # matcher actually decided on and what every live consumer (Smart Termination, + # the ML end-guard, ETA, the panel) is calibrated against. But the two can be + # different members: `collapse_group_candidates` keeps the top sibling's record + # and `_stage5_pick_member` then chooses by integrated energy. Measured on the + # real corpus, the pick differs from the top sibling in 16.7% of whole-cycle + # group wins, by up to 0.157 of score. So a *label* decision - which turns the + # cycle into evidence for that one member - gets its own number. + member_confidence: float | None = None + + @property + def label_confidence(self) -> float: + """The confidence a LABEL / persistence decision may rely on. + + Identical to ``confidence`` for every match except a Stage-5 group win + whose selected member scored lower than the sibling whose score became the + group's. There, labelling the cycle makes it evidence for the *selected* + member, so the bar has to be what that member earned itself, not what its + sibling did. Both numbers are blended pipeline scores over the same trace, + so they are directly comparable (unlike ``stage5_member_fit``, which is + Stage-2 only). ``min`` is defensive: the group score is by construction the + max over the members, so it is already the larger of the two. + + Falls back to ``confidence`` when the member has no score of its own (the + matcher rejected it at Stage 1, yet Stage 5 still selected it) - there is no + comparable number to gate on, and the existing ambiguity / overrun guards + still apply. + """ + if self.member_confidence is None: + return self.confidence + return min(self.confidence, self.member_confidence) def to_dict(self) -> dict[str, Any]: """Convert to dictionary with JSON-serializable types, excluding heavy arrays.""" @@ -870,6 +908,49 @@ class WashDataStore(Store[JSONDict]): return old_data +def collapse_group_candidates( + candidates: list[dict], group_members: dict[str, list[str]] +) -> list[dict]: + """Replace each cohesive group's scored members with ONE candidate carrying + the best member's score (#400). + + Members reach the matcher as their own snapshots, so each is scored on its own + curve; the group is formed afterwards, here. That is the difference from the + original design, which averaged the member curves into an aggregate snapshot + *before* scoring - on a temperature family (same silhouette, different heating + length) the mean curve carries an energy figure no member has and roughly + doubles the MAE, so the family could lose the program-level match outright and + Stage-5 member selection never ran. + + What grouping is actually for survives: the members no longer sit next to each + other in the ranking, so they cannot make each other look ambiguous. The winner + keeps the group's ``__group__`` name, so the existing Stage-5 hand-off (and + with it `_stage5_pick_member`, register item 99) is unchanged. + + Pure; ``candidates`` must already be score-sorted, and the returned list keeps + that order because entries are only dropped, never reordered. + """ + if not group_members or not candidates: + return candidates + member_to_group: dict[str, str] = { + m: key for key, members in group_members.items() for m in members + } + if not member_to_group: + return candidates + out: list[dict] = [] + seen_groups: set[str] = set() + for cand in candidates: + key = member_to_group.get(str(cand.get("name"))) + if key is None: + out.append(cand) + continue + if key in seen_groups: + continue # a lower-scoring sibling; the family is already represented + seen_groups.add(key) + out.append({**cand, "name": key, "group_best_member": cand.get("name")}) + return out + + def _ambiguity_from_candidates(candidates: list[dict]) -> tuple[float, bool]: """Top1-vs-top2 score margin and whether the match is ambiguous. @@ -1485,6 +1566,9 @@ class ProfileStore: if not isinstance(log, list): log = [] self._data["settings_changelog"] = log + # Snapshot for the rollback below. Shallow copy is enough: entries are only + # ever inserted or dropped wholesale, never edited in place. + before = list(log) now_iso = dt_util.now().isoformat() added = False @@ -1509,7 +1593,16 @@ class ProfileStore: if len(log) > self.SETTINGS_CHANGELOG_MAX: del log[self.SETTINGS_CHANGELOG_MAX:] self._data["settings_changelog"] = log - await self.async_save() + try: + await self.async_save() + except Exception: + # Roll the insert back rather than leave an entry that was never + # persisted: get_settings_changelog() reads straight off _data, so the + # failed audit line would be shown as history and then written for real + # by whatever saves next. Restored here rather than at the call sites so + # every caller gets it, and the snapshot is taken before any mutation. + self._data["settings_changelog"] = before + raise # ─── On-device ML model versions (Stage 4) ──────────────────────────────── @@ -1887,6 +1980,19 @@ class ProfileStore: wanted = tuple(PROFILE_EVIDENCE_SOURCES) self._evidence_sources = wanted + @property + def save_debug_traces(self) -> bool: + """Whether rich per-cycle debug traces are persisted with each cycle. + + Pushed in by the manager from entry options, the way energy_mode and + evidence_sources are; the store has no access to them itself. + """ + return self._save_debug_traces + + @save_debug_traces.setter + def save_debug_traces(self, value: Any) -> None: + self._save_debug_traces = bool(value) + def iter_evidence_cycles(self) -> list[CycleDict]: """Stored cycles the user allows to shape a profile. @@ -2185,66 +2291,39 @@ class ProfileStore: def _grouped_snapshots( self, snapshots: list[dict[str, Any]] ) -> tuple[list[dict[str, Any]], dict[str, list[str]], dict[str, dict[str, Any]]]: - """Collapse each *cohesive* group into one aggregate candidate snapshot. + """Map each *cohesive* group to its member snapshots for the Stage-5 pass. - Returns (snapshots, group_members, member_snaps). Groups with <2 present - members, or cohesion below GROUP_MIN_COHESION (too generic an aggregate), - are left as individual member snapshots. Aggregate name is prefixed - ``__group__`` and mapped to its member profile names in group_members. + Returns (snapshots, group_members, member_snaps). Snapshots are returned + **unchanged**: every member is scored on its own curve, and the family is + formed afterwards by ``collapse_group_candidates`` (#400). Groups with <2 + present members, or cohesion below GROUP_MIN_COHESION, are not mapped at + all, so their members simply stay individual candidates. + + Until #400 this method averaged the member curves into one ``__group__`` + aggregate snapshot. That is what the collapse replaces: on a temperature + family the averaged curve belongs to no member (it has a half-height + heating block and an energy figure neither member reaches), which cost the + family the program-level match before Stage 5 could pick between them. """ groups = self.get_profile_groups() if not groups: return snapshots, {}, {} by_name = {s["name"]: s for s in snapshots} - member_to_group: dict[str, str] = {} + group_members: dict[str, list[str]] = {} + member_snaps: dict[str, dict[str, Any]] = {} for gname, g in groups.items(): present = [m for m in (g.get("members") or []) if m in by_name] if len(present) < 2 or self.group_cohesion(present) < GROUP_MIN_COHESION: continue # keep members individual + group_members[f"__group__{gname}"] = present for m in present: - member_to_group[m] = gname - if not member_to_group: - return snapshots, {}, {} - n = 200 - agg_curves: dict[str, list[np.ndarray]] = {} - agg_durs: dict[str, list[float]] = {} - agg_spans: dict[str, list[float]] = {} - member_snaps: dict[str, dict[str, Any]] = {} - out: list[dict[str, Any]] = [] - for s in snapshots: - g = member_to_group.get(s["name"]) - if g is None: - out.append(s) - continue - member_snaps[s["name"]] = s - sp = s.get("sample_power") or [] - if len(sp) >= 2: - arr = np.asarray(sp, dtype=float) - agg_curves.setdefault(g, []).append( - np.interp(np.linspace(0, 1, n), np.linspace(0, 1, arr.size), arr) - ) - _dur = float(s.get("avg_duration") or 0.0) - agg_durs.setdefault(g, []).append(_dur) - # Members are resampled onto a normalized 0..1 axis above, so the - # aggregate's own time span is the mean of its members' (#364). - agg_spans.setdefault(g, []).append(float(s.get("sample_span_s") or _dur)) - group_members: dict[str, list[str]] = {} - for g, curves in agg_curves.items(): - key = f"__group__{g}" - durs = [d for d in agg_durs[g] if d > 0] - spans = [v for v in agg_spans.get(g, []) if v > 0] - out.append({ - "name": key, - "avg_duration": float(np.mean(durs)) if durs else 0.0, - "sample_power": np.mean(np.array(curves), axis=0).tolist(), - "sample_span_s": float(np.mean(spans)) if spans else 0.0, - }) - group_members[key] = [m for m in member_to_group if member_to_group[m] == g and m in member_snaps] - return out, group_members, member_snaps + member_snaps[m] = by_name[m] + return snapshots, group_members, member_snaps def _stage5_pick_member( self, current_power: list[float], current_duration: float, members: list[str], member_snaps: dict[str, dict[str, Any]], + in_progress: bool = False, ) -> tuple[str, float | None, float | None]: """Within a winning group, pick the member whose integrated ENERGY best matches the cycle. @@ -2260,6 +2339,15 @@ class ProfileStore: duration-cohesive, so duration only adds noise). Duration is still returned (for ETA and the overrun guard) but is intentionally not part of selection. + ``in_progress`` marks a live, still-running cycle and makes the comparison + like-for-like, exactly as Stage 4 does (#400): each member is integrated + only up to the elapsed time via the shared ``analysis.prefix_mean``. Without + it the partial cycle's energy is graded against every member's COMPLETE + energy, and since a partial figure is always the smaller one the group + resolves to its coolest member for most of every run - the same defect this + stage exists to fix, one stage later. Off at cycle end, where the whole-cycle + comparison is the correct one and item 99's validation applies unchanged. + Returns (member_name, individual_fit_score, member_avg_duration). The fit score is the chosen member's own alignment score, used as a sanity check.""" cur = np.asarray(current_power, dtype=float) @@ -2286,7 +2374,13 @@ class ProfileStore: if sp.size == 0: continue md = float(snap.get("avg_duration") or 0.0) - mem_energy = float(sp.mean()) * md + if in_progress: + mem_mean, mem_span = analysis.prefix_mean( + sp, current_duration, float(snap.get("sample_span_s") or md or 0.0), md + ) + mem_energy = mem_mean * mem_span + else: + mem_energy = float(sp.mean()) * md # Scale 0.30 is validated as insensitive over 0.25-0.40 on real data. sc = agree(cur_energy, mem_energy, 0.30) if sc > best_sc: @@ -2454,29 +2548,127 @@ class ProfileStore: await self.async_save() def get_lifetime_cycle_count(self) -> int: - """Persisted monotonic lifetime completed-cycle count. + """Cycles this appliance has run, ever - the odometer. - Only ever increments (never regresses when history is trimmed/merged), so it - is the correct basis for milestone crossings. Pure getter - never persists. - Never raises; returns 0 when unset. + Only ever increments (never regresses when history is trimmed, merged or + deleted from), so it is the correct basis for milestone crossings, the + maintenance reminders and the cycle-count sensor (#414). + + Floored at ``len(past_cycles)``: a stored record is evidence of a run, so + the odometer can never read below the number of records on hand. That makes + the getter correct even before :meth:`_heal_lifetime_cycle_count` has + persisted the floor (a v8->v9 seed predating a replace-mode import), and it + stops a hand-correction from being quietly undone at the next restart. Pure + getter - never persists. Never raises; returns 0 when unset. """ try: - return int(self._data.get("lifetime_cycle_count", 0) or 0) + stored = int(self._data.get("lifetime_cycle_count", 0) or 0) except Exception: # noqa: BLE001 - return 0 + stored = 0 + try: + return max(stored, len(self.get_past_cycles())) + except Exception: # noqa: BLE001 + return stored - def set_lifetime_cycle_count(self, count: int) -> None: + def set_lifetime_cycle_count(self, count: int, *, force: bool = False) -> None: """Set the lifetime cycle count in memory WITHOUT persisting. The cycle-end path batches this with the immediately-following lifetime energy save (:meth:`async_add_lifetime_energy_wh`) so the store is written once per cycle. Encapsulates the storage key so callers need not touch ``_data`` directly. Ignores non-integer input. + + This is an odometer: it changes when a cycle persists, or when the user + explicitly corrects it. A decrease is therefore refused unless ``force`` + is set (the user-correction path, WS ``set_lifetime_cycle_count``), so no + ordinary code path can walk the appliance's life total backwards - the bug + behind discussion #414, where deleting a record lowered the count and an + external "every 30 cycles" maintenance task stopped firing. """ try: - self._data["lifetime_cycle_count"] = int(count) + value = int(count) except (TypeError, ValueError): - pass + return + if value < 0: + return + if not force and value < self.get_lifetime_cycle_count(): + return + self._data["lifetime_cycle_count"] = value + + def _heal_lifetime_cycle_count(self) -> None: + """Persist the ``len(past_cycles)`` floor that the getter already applies. + + The v8->v9 migration seeded the counter from the history that existed at + that moment, and a replace-mode selective import can overwrite history + without its ``lifetime_stats``. Either way the appliance has demonstrably + run at least as many cycles as are stored. Writing the floor down at load + keeps the stored key in step with + :meth:`get_lifetime_cycle_count`, so an export carries the real reading. + Only ever increases. Never raises. + """ + try: + try: + stored = int(self._data.get("lifetime_cycle_count", 0) or 0) + except (TypeError, ValueError, OverflowError): + # Same fallback :meth:`get_lifetime_cycle_count` uses: an unusable + # stored value is no reading at all, so the floor below replaces it + # outright. Skipping the heal instead left the malformed value in + # ``_data``, which ``export_data`` copies verbatim, so it travelled + # into the export and back in on the next import. + self._logger.debug( + "Lifetime cycle count %r is not a number; replacing it with the " + "stored-history floor", + self._data.get("lifetime_cycle_count"), + ) + stored = 0 + real = len(self.get_past_cycles()) + if real > stored: + self._data["lifetime_cycle_count"] = real + self._logger.debug( + "Healed lifetime cycle count %d -> %d (stored history is larger)", + stored, + real, + ) + except Exception: # noqa: BLE001 + self._logger.debug("Lifetime cycle count heal failed", exc_info=True) + + # ------------------------------------------------------------------ + # Armed program (pre-arm a manual program for the next cycle, #411) + # ------------------------------------------------------------------ + + def get_armed_program(self) -> str | None: + """Return the program the user pinned for the current or next cycle. + + Persisted because arming is an idle-time action: the user picks the + programme now and starts the machine later, so it has to survive a Home + Assistant restart in between. (The mid-cycle pin rides the active-cycle + snapshot instead, see ``manual_program_name``.) Never raises. + """ + try: + value = self._data.get("armed_program") + return value if isinstance(value, str) and value else None + except Exception: # noqa: BLE001 + return None + + async def async_set_armed_program(self, program: str | None) -> None: + """Persist (or clear, with ``None``) the armed program. Never raises.""" + try: + if isinstance(program, str) and program: + self._data["armed_program"] = program + else: + self._data.pop("armed_program", None) + await self.async_save() + except Exception: # noqa: BLE001 + # In-memory arming (manager._armed_program) has already taken effect and + # is what applies the program to the next cycle, so this is not a failed + # arm - it is an arm that will not survive a restart. Worth a warning + # rather than a debug line, because the user sees no error and would have + # no way to explain the choice being gone after a restart. + self._logger.warning( + "Failed to persist the armed program; it stays armed for this session " + "but will not survive a Home Assistant restart", + exc_info=True, + ) # ------------------------------------------------------------------ # E1: Maintenance log & predictive-maintenance reminders (Group E) @@ -2485,7 +2677,8 @@ class ProfileStore: def get_maintenance_log(self) -> list[dict[str, Any]]: """Return logged maintenance events, most-recent-first. Never raises. - Each entry is ``{"id", "date", "event_type", "notes"}``. Entries with an + Each entry is ``{"id", "date", "event_type", "notes", "cycle_count_at_log"}`` + (the last is absent on entries logged before #414). Entries with an unparseable date sort last. """ try: @@ -2513,11 +2706,31 @@ class ProfileStore: """ if event_type not in MAINTENANCE_EVENT_TYPES: raise ValueError(f"Unknown maintenance event_type: {event_type!r}") + when = date if isinstance(date, str) and date else dt_util.now().isoformat() + # Reject a date the log cannot read back, for the same reason event_type is + # validated above: an unparseable date poisons the entry twice over. It makes + # the stamp below rewind the odometer by the WHOLE retained history (the + # parser returns None and `_odometer_at(None)` reads that as "since the + # beginning of time"), and `cycles_since_maintenance` then skips the entry + # when picking the latest event for its type - so the task reports as never + # serviced and its reminder comes due immediately. The WS handler already + # maps ValueError to `invalid_format`. + parsed_when = _parse_maintenance_dt(when) + if parsed_when is None: + raise ValueError(f"Unparseable maintenance date: {when!r}") entry: dict[str, Any] = { "id": uuid.uuid4().hex[:12], - "date": date if isinstance(date, str) and date else dt_util.now().isoformat(), + "date": when, "event_type": event_type, "notes": str(notes or ""), + # Odometer reading at the moment of servicing (#414). "Cycles since" is + # then a subtraction against the monotonic lifetime counter instead of a + # scan of `past_cycles`, which is capped at max_past_cycles and shrinks + # when the user deletes a record - both of which silently froze the + # reminder. Derived from the event's own date rather than from "now", so + # a back-dated entry ("descaled it three months ago") still reports the + # cycles run since that date instead of collapsing to zero. + "cycle_count_at_log": self._odometer_at(parsed_when), } log = self._data.setdefault("maintenance_log", []) if not isinstance(log, list): @@ -2616,18 +2829,103 @@ class ProfileStore: self._logger.info("Deleted playground preset %r", name) return True - def cycles_since_maintenance(self, event_type: str) -> int: - """Count completed cycles since the most recent maintenance event of a type. + def _cycles_after( + self, since: datetime | None, *, include_all_statuses: bool = False + ) -> int: + """Stored cycles that started after *since* (all of them if None). - Counts completed cycles (``status == "completed"``) whose ``start_time`` is - after the most recent maintenance event of ``event_type``. If no such event - was ever logged, returns the total completed-cycle count. Never raises. + A scan of the retained history, so it is bounded by ``max_past_cycles`` and + shrinks when a record is deleted. Used only to place a maintenance event on + the odometer at log time, and as the legacy fallback for events logged + before that stamp existed. Never raises. + + ``include_all_statuses`` selects the basis, and the two callers need + different ones. :meth:`_odometer_at` rewinds the lifetime odometer, which + counts every persisted cycle regardless of status, so it must subtract on + that same basis or the rewind is short and the stamp it writes is too high. + The legacy date-scan fallback keeps the completed-only basis it was + written with, so entries logged before the stamp existed keep answering + the way they always did. """ try: - completed = [ + cycles = [ c for c in self.get_past_cycles() - if isinstance(c, dict) and c.get("status") == "completed" + if isinstance(c, dict) + and (include_all_statuses or c.get("status") == "completed") ] + if since is None: + return len(cycles) + count = 0 + for c in cycles: + start = _parse_start_dt(c.get("start_time")) + if start is None: + continue + # A naive stamp cannot be compared with the aware `since` at all + # ("can't compare offset-naive and offset-aware datetimes"), and an + # ISO string without an offset parses naive - reachable through an + # import or a hand-edited file even though every record this device + # writes is aware. Read as UTC, the same way `newest` does in + # async_repair_profile_samples. + if start.tzinfo is None: + start = start.replace(tzinfo=dt_util.UTC) + # Guarded per cycle, not once around the loop: the outer handler + # below returns 0 for the WHOLE tally, and 0 means "no cycles since + # that date", so a single odd record used to make _odometer_at stamp + # the CURRENT odometer onto a back-dated event. cycles_since_maintenance + # then subtracts that stamp and the reminder is late by every cycle + # run since the service. + try: + if start > since: + count += 1 + except TypeError: # pragma: no cover - defensive + continue + return count + except Exception: # noqa: BLE001 + return 0 + + def _odometer_at(self, when: datetime | None) -> int: + """The lifetime cycle count as it stood at *when*. + + ``odometer_now - cycles_after(when)``, clamped to >= 0. For "now" this is + simply the current reading; for a back-dated maintenance entry it rewinds + the odometer by the cycles run since that date. + + The rewind counts every persisted status, matching what the odometer itself + counts. Rewinding on completed-only left the stamp too high by the number of + interrupted or force-stopped runs in between, and since + :meth:`cycles_since_maintenance` subtracts that stamp, the reminder came due + late by the same amount. Never raises. + """ + try: + return max( + 0, + self.get_lifetime_cycle_count() + - self._cycles_after(when, include_all_statuses=True), + ) + except Exception: # noqa: BLE001 + return 0 + + def cycles_since_maintenance(self, event_type: str) -> int: + """Cycles this appliance has run since it was last serviced for *event_type*. + + Measured against the monotonic lifetime odometer + (:meth:`get_lifetime_cycle_count`): the reading now minus the reading stamped + on the most recent matching event. That is retention-proof and + deletion-proof, which the previous ``past_cycles`` scan was not - past + ``max_past_cycles`` it stopped rising and every deleted record set the + reminder back (#414). + + Events logged before the stamp existed have no reading to subtract, so they + fall back to the old date-based scan. When the task was never logged at all + (the common case), the answer is the whole odometer. + + Note the odometer counts every persisted cycle, including ones recorded as + interrupted or force-stopped, where the old scan counted only completed + ones: a filter still gets dirty on a run the watchdog had to close. + Never raises. + """ + try: + latest: dict[str, Any] | None = None latest_dt: datetime | None = None for e in self._data.get("maintenance_log", []) or []: if not isinstance(e, dict) or e.get("event_type") != event_type: @@ -2635,14 +2933,13 @@ class ProfileStore: dt = _parse_maintenance_dt(e.get("date")) if dt is not None and (latest_dt is None or dt > latest_dt): latest_dt = dt - if latest_dt is None: - return len(completed) - count = 0 - for c in completed: - start = _parse_start_dt(c.get("start_time")) - if start is not None and start > latest_dt: - count += 1 - return count + latest = e + if latest is None: + return self.get_lifetime_cycle_count() + stamp = latest.get("cycle_count_at_log") + if isinstance(stamp, (int, float)) and not isinstance(stamp, bool): + return max(0, self.get_lifetime_cycle_count() - int(stamp)) + return self._cycles_after(latest_dt) except Exception: # noqa: BLE001 return 0 @@ -3047,6 +3344,120 @@ class ProfileStore: except Exception: # noqa: BLE001 return {} + def unmatchable_profiles(self) -> dict[str, str]: + """Profiles the matcher cannot even consider, mapped to the reason. + + ``_build_match_snapshots`` admits a profile to the candidate pool from one of + two sources: its multi-cycle envelope, or a single sample cycle (its pinned + ``sample_cycle_id``, else any labelled evidence cycle). A profile with neither + is skipped with a ``debug`` log and nothing else - it can never win a match, + and it cannot veto a shorter look-alike through the #364 prefix guard either, + since that guard only inspects candidates that made it into the ranking. #400 + was reported against exactly that state: five wrong programs won in turn while + the right one sat outside the contest, and the cycle was then cut at 0.98x the + wrong program's length. + + This is a *sufficient* condition, deliberately weaker than the builder's own + skip list: it reports a profile only when BOTH sources are unusable, so it + never claims a profile is unmatchable when the builder would have admitted it. + (What it does not predict is a resample failure on a cycle that does have data.) + ``test_issue_400_label_provenance`` pins it against a real match so the two + cannot drift apart. + + Reads the *evidence* view, so a profile whose only cycles the user excluded via + the evidence-source setting is reported too - that is genuinely the state they + have put it in. Never raises; returns ``{}`` on error. + """ + try: + profiles = self._data.get("profiles") or {} + if not isinstance(profiles, dict) or not profiles: + return {} + + # Same two lookups the snapshot builder precomputes, same selections. + by_id: dict[str, CycleDict] = {} + labeled_by_profile: dict[str, CycleDict] = {} + for c in self.iter_evidence_cycles(): + cid = c.get("id") + if cid is not None and cid not in by_id: + by_id[cid] = c + pname = c.get("profile_name") + if not pname or not c.get("power_data"): + continue + if ( + pname not in labeled_by_profile + and c.get("status") in ("completed", "force_stopped") + ): + labeled_by_profile[pname] = c + + envelopes = self._data.get("envelopes") or {} + out: dict[str, str] = {} + + for name, profile in profiles.items(): + if not isinstance(profile, dict): + continue + + # Source 1: the envelope average curve (needs >= 2 cycles and a duration). + env = envelopes.get(name) if isinstance(envelopes, dict) else None + env_avg = env.get("avg") if isinstance(env, dict) else None + env_usable = False + if ( + isinstance(env, dict) + and int(env.get("cycle_count") or 0) >= 2 + and isinstance(env_avg, list) + and len(env_avg) > 1 + and isinstance(env_avg[0], (list, tuple)) + and len(env_avg[0]) >= 2 + ): + try: + span = float(env_avg[-1][0]) - float(env_avg[0][0]) + except (TypeError, ValueError, IndexError): + span = 0.0 + env_usable = bool( + env.get("target_duration") or profile.get("avg_duration") or span + ) + + # Source 2: a single sample cycle. + sample_id = profile.get("sample_cycle_id") + sample = by_id.get(sample_id) if sample_id else None + if sample is None or not sample.get("power_data"): + sample = labeled_by_profile.get(name) + sample_power_data = (sample or {}).get("power_data") or [] + sample_present = bool(sample is not None and sample_power_data) + sample_usable = sample_present and bool( + profile.get("avg_duration") + or (sample or {}).get("duration") + # Third fallback in _build_match_snapshots: when both stored + # durations are falsy it derives one from the RESAMPLED + # segment's timestamp span, so a profile with neither can still + # be admitted. Deliberately not reproduced exactly - that needs + # _get_cached_sample_segment, whose longest-gap-free-run pick + # this method cannot afford on the event loop - so any sample + # with two readings counts as usable. That is the weaker + # direction this method documents: never claim a profile is + # unmatchable where the builder might admit it. + or len(sample_power_data) >= 2 + ) + + if env_usable or sample_usable: + continue + # "no usable duration" means evidence EXISTS and only the length is + # missing, so the advisory can tell the user to set one. An envelope + # curve alone does not qualify: below 2 cycles the builder never + # admits it, so there is nothing to size a match against either way. + env_has_evidence = ( + isinstance(env, dict) + and int(env.get("cycle_count") or 0) >= 2 + and isinstance(env_avg, list) + ) + out[name] = ( + "no usable duration" + if (sample_present or env_has_evidence) + else "no evidence cycle with power data" + ) + return out + except Exception: # noqa: BLE001 + return {} + def compute_profile_advisories(self) -> list[dict[str, Any]]: """Actionable per-profile maintenance advisories from existing signals. @@ -3062,6 +3473,37 @@ class ProfileStore: health = self.compute_profile_health() or {} trends = self.compute_profile_trends() or {} + # A program that cannot be matched at all comes first: no other advice about + # it means anything, and until now this state was invisible (a debug log in + # the snapshot builder). See :meth:`unmatchable_profiles`. + unmatchable = self.unmatchable_profiles() or {} + for name, reason in unmatchable.items(): + # Two reasons, two different things to do about it. Telling a user + # whose profile HAS a cycle to go and record one sends them after a + # problem they do not have. + if reason == "no usable duration": + message = ( + f"'{name}' can never be matched: it has a cycle with power " + "data but no usable duration, so WashData has no length to " + "size a match against. Re-record this program, or set its " + "expected duration, to bring it back into matching." + ) + message_key = "msg.advisory_unmatchable_no_duration" + else: + message = ( + f"'{name}' can never be matched: it has no cycle with power " + "data behind it, so WashData has nothing to compare a running " + "cycle against. Label a cycle of this program (or record one) " + "to bring it back into matching." + ) + message_key = "msg.advisory_unmatchable" + advisories.append({ + "profile": name, "severity": "warning", "code": "unmatchable", + "message": message, + "message_key": message_key, + "message_params": {"name": name}, + }) + for name, h in health.items(): if h.get("health_status") == "poor": advisories.append({ @@ -3616,6 +4058,8 @@ class ProfileStore: data = await self._store.async_load() if data: self._data = data + # The odometer can only be behind, never ahead, of the stored history (#414). + self._heal_lifetime_cycle_count() # Ensure legacy custom phase formats are normalized in-memory. self._get_shared_custom_phases() # Assign ids to any custom phase missing one. @@ -3637,11 +4081,22 @@ class ProfileStore: """Repair profile sample references after retention or migrations. Ensures each profile's sample_cycle_id points to an existing cycle that still - has full-resolution power_data. If missing, picks the newest available cycle - with power_data and assigns it as the sample (and labels that cycle to the - profile if it was unlabeled). + has full-resolution power_data. If it does not, the profile is re-pointed at the + newest cycle **already labelled to it** - real, imported or backfilled, so an + import-only profile is repaired from its own cycles rather than left broken. - Returns stats dict. + What this deliberately does NOT do (#400 follow-up): adopt an *unlabelled* + cycle. It used to hand each sampleless profile the newest unlabelled cycle - + any cycle, no similarity check - as its template, its avg_duration and its + label. Since this runs on every setup, a program left without evidence (its + cycles deleted, unlabelled via the Cleanup tab, or excluded by the evidence- + source setting) silently adopted an unrelated wash at the next restart, and then + matched and mis-estimated against it. A profile with no cycles of its own is a + legitimate pending state (``cleanup_orphaned_profiles`` keeps it on purpose); + it is reported by :meth:`unmatchable_profiles` instead of being papered over. + + Returns stats dict. ``cycles_labeled_as_sample`` is retained for diagnostics + continuity and is now always 0. """ stats = { "profiles_checked": 0, @@ -3651,7 +4106,7 @@ class ProfileStore: profiles: dict[str, dict[str, Any]] = self._data.get("profiles", {}) or {} cycles: list[dict[str, Any]] = self._data.get("past_cycles", []) or [] - if not profiles or not cycles: + if not profiles: return stats # Sample validity must recognise imported and backfilled cycles: an import-only @@ -3662,14 +4117,40 @@ class ProfileStore: c["id"]: c for c in self.iter_stored_cycles() if c.get("id") } - def newest_unlabeled_with_power_data() -> dict[str, Any] | None: - candidates: list[dict[str, Any]] = [ - c for c in cycles if c.get("power_data") and not c.get("profile_name") - ] + # The "no evidence at all" bail-out is taken on every stored list, not on + # past_cycles. A device whose history is entirely imported or backfilled has an + # empty past_cycles, so guarding on that list returned before the loop and left + # exactly the import-only profile this method exists to repair broken. + if not by_id: + return stats + + def newest(candidates: list[dict[str, Any]]) -> dict[str, Any] | None: + """The most recent candidate, ordered by parsed instant. + + Not by raw ``start_time`` string: ISO stamps only sort chronologically + when they share a UTC offset, and one store legitimately holds several. + The reference library carries +02:00, +01:00 and +00:00 side by side + (DST either side of a transition, plus downloaded community cycles + recorded in someone else's zone), and e.g. ``11:00+02:00`` sorts above + ``10:00+00:00`` while being an hour earlier. Repair would then adopt an + older cycle's trace and duration as the profile's sample. + + Unparseable stamps sort last rather than raising, and a naive stamp is + read as UTC so it can be compared with the aware ones at all. + """ if not candidates: return None + + def _key(c: dict[str, Any]) -> datetime: + parsed = _parse_start_dt(c.get("start_time")) + if parsed is None: + return datetime.min.replace(tzinfo=dt_util.UTC) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=dt_util.UTC) + return parsed + try: - return max(candidates, key=lambda c: c.get("start_time", "")) + return max(candidates, key=_key) except Exception: # pylint: disable=broad-exception-caught return candidates[-1] @@ -3682,35 +4163,30 @@ class ProfileStore: if sample and sample.get("power_data"): continue - # Prefer newest already-labeled cycle for this profile that still has power_data - labeled_candidates = [ + # Newest cycle already labelled to this profile that still has power_data. + # Real cycles first (they are this appliance's own evidence); an imported or + # backfilled cycle is a valid template too, and for an import-only profile + # it is the ONLY one - searching past_cycles alone left such a profile + # unrepairable, which is what made the unlabelled-steal look necessary. + chosen = newest([ c for c in cycles if c.get("profile_name") == profile_name and c.get("power_data") - ] - if labeled_candidates: - try: - chosen = max( - labeled_candidates, key=lambda c: c.get("start_time", "") - ) - except Exception: # pylint: disable=broad-exception-caught - chosen = labeled_candidates[-1] - else: - # Fallback: pick newest UNLABELED cycle with power_data - chosen = newest_unlabeled_with_power_data() + ]) or newest([ + c + for c in self.iter_stored_cycles() + if c.get("profile_name") == profile_name and c.get("power_data") + ]) if not chosen: + # No evidence of its own: leave the profile alone (pending state) so it + # cannot adopt an unrelated cycle's shape and duration. continue profile["sample_cycle_id"] = chosen.get("id") if chosen.get("duration"): profile["avg_duration"] = chosen["duration"] - # If chosen cycle is unlabeled, label it to this profile to bootstrap matching - if not chosen.get("profile_name"): - chosen["profile_name"] = profile_name - stats["cycles_labeled_as_sample"] += 1 - stats["profiles_repaired"] += 1 try: await self.async_rebuild_envelope(profile_name) @@ -4844,18 +5320,23 @@ class ProfileStore: @staticmethod def _envelope_time_power( envelope: JSONDict | None, + curve_key: str = "avg", ) -> tuple[list[float], list[float]] | None: - """Extract (time_grid, power) from an envelope's ``avg`` curve, or None. + """Extract (time_grid, power) from an envelope curve, or None. Handles both the new ``[[t, p], ...]`` and legacy ``[p, ...]`` formats (reconstructing the grid from ``time_grid`` / ``target_duration`` / a 60 s fallback). Single source of the envelope time axis for both the alignment worker and the Smart-Termination release span (#348), so the mapped position and the span it is compared against always live on the same grid. + + ``curve_key`` selects the band: ``"avg"`` (the default, every existing + caller) or ``"max"``, which #399 uses to ask whether ANY member of the + profile ends with a high-power block. """ if not envelope: return None - env_avg_raw = envelope.get("avg", []) + env_avg_raw = envelope.get(curve_key, []) if not env_avg_raw: return None try: @@ -4985,6 +5466,147 @@ class ProfileStore: except Exception: # noqa: BLE001 return None + def profile_terminal_high_block( + self, + profile_name: str, + threshold_w: float, + ) -> tuple[float, float, float] | None: + """Position and length of the LAST contiguous run above ``threshold_w`` in a + profile's own trace: ``(start_frac, seconds, start_offset_s)``, or None (#399). + + ``start_frac`` is where that run begins as a fraction of the profile's + quiet-trimmed span, and answers only one question: is this block *terminal* + (``ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC``)? ``start_offset_s`` is the same + position in ABSOLUTE seconds from the start of the trace, and is what the + detector scans from. This is what tells the anti-crease finalise that a + programme ends with a spin, and roughly how long that spin runs, so it can + wait for the live counterpart instead of closing the wash seconds before it. + + The two are deliberately on different bases, because they answer different + questions and the caller has no denominator that fits both (register item + 196). The fraction MUST divide by the trimmed span, or a capture's idle tail + pushes a genuine terminal spin under the gate and disarms the guard (see the + trim comment below). The scan offset must NOT be reconstructed from it as + ``start_frac x expected``, because ``expected`` is the profile's + ``avg_duration``, which tracks the UNTRIMMED span - measured over 152 real + washer/dryer cycles a recorded ``duration`` sits at relative error 0.0000 + (median) from the full span versus 0.0274 from the trimmed one. Multiplying + a trimmed-basis fraction by an untrimmed-basis duration produced a + systematically LATE offset (median +180 s, max +1731 s over 13 real armed + profiles), so the run's own spin fell before the scan window and was never + counted: over 36 real armed profile/cycle pairs the product recognised the + spin 3 times, the absolute offset 14. Returning the offset removes the + denominator from the question entirely. + + Reads the envelope's ``max`` band first - a spin recorded by ANY member is + a spin this programme can have - and falls back to the sample cycle's raw + trace, because a thinly-trained profile (one labelled cycle, so no envelope) + is still a match candidate and would otherwise get no guard at all. Pure + statistics, never raises; same contract as ``profile_tail_power``. + """ + try: + level = float(threshold_w) + if not (level > 0): + return None + + curves = self._envelope_time_power(self.get_envelope(profile_name), "max") + if not curves or len(curves[0]) < 2: + profile = self.get_profiles().get(profile_name) + if not isinstance(profile, dict): + return None + sample_id = profile.get("sample_cycle_id") + if not sample_id: + return None + cycle, _ = self.find_stored_cycle(str(sample_id)) + if not cycle: + return None + # Through decompress_power_data, not raw power_data: a legacy cycle + # stores (iso_string, power) pairs (see profile_tail_power). + points = decompress_power_data(cycle) + if len(points) < 2: + return None + times = [float(pt[0]) for pt in points] + powers = [float(pt[1]) for pt in points] + else: + times, powers = curves + + # Trim trailing near-silence before measuring positions. How much idle + # tail a stored trace carries is a property of the capture, not the + # appliance - the same programme on the same machine ranges from 0 s to + # 613 s of trailing quiet in the #399 reporter's own store - and it is + # what makes profile_tail_power's last-5% mean swing by two orders of + # magnitude. Left in, it would push a genuine terminal spin below + # ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC and silently disarm the guard. + # Floor is relative to the trace's own peak, so it needs no config and + # keeps a real low-power tumble tail (which legitimately means the block + # is not terminal). + peak = max(powers) if powers else 0.0 + quiet_floor = max(1.0, peak * 0.02) + end = len(powers) - 1 + while end > 0 and powers[end] <= quiet_floor: + end -= 1 + if end < 1: + return None + times = times[: end + 1] + powers = powers[: end + 1] + + span = float(times[-1]) - float(times[0]) + if span <= 0: + return None + # Walk back to the last sample above the level, then back to the start + # of the contiguous run it belongs to. + last = None + for i in range(len(powers) - 1, -1, -1): + if powers[i] > level: + last = i + break + if last is None: + return None # this programme never draws above the level + first = last + while first > 0 and powers[first - 1] > level: + first -= 1 + # Absolute seconds from the start of the trace. `times` is already + # quiet-trimmed, but the trim is TRAILING only, so this offset is + # identical on the trimmed and untrimmed traces - which is exactly the + # capture-tail invariance the fraction needs the trim to get, obtained + # here for free (see the item-196 note in the docstring). + start_offset = float(times[first]) - float(times[0]) + start_frac = start_offset / span + # The block covers the interval up to the sample AFTER its last one, so + # a single-sample spike still has a length (its own step). When the run + # reaches the trimmed trace's final sample there IS no following one, so + # the step is carried over from the preceding interval instead of + # clamping back onto times[last]: clamping made the block one step short, + # and for a single-sample spike it made it zero-length, which fell + # through the guard below and returned None. That silently disarmed the + # anti-crease spin wait for a profile whose spin runs to the end of its + # own trace - the shape this method exists to detect. + if last + 1 < len(times): + end_t = float(times[last + 1]) + elif last > 0: + # No trailing sample, so the final step has to be estimated. Use the + # trace's own MEDIAN positive interval rather than the immediately + # preceding one: on an irregular trace that neighbour can be an + # outage-sized gap, and copying it verbatim reported a block hours + # longer than the one actually observed, inflating `needed` until the + # anti-crease wait ran to its ceiling. The median is representative + # by construction and cannot be dragged by a single gap. + diffs = [ + float(times[i + 1]) - float(times[i]) + for i in range(len(times) - 1) + if float(times[i + 1]) - float(times[i]) > 0 + ] + step = statistics.median(diffs) if diffs else 0.0 + end_t = float(times[last]) + step + else: + end_t = float(times[last]) + seconds = end_t - float(times[first]) + if seconds <= 0: + return None + return (min(max(start_frac, 0.0), 1.0), seconds, start_offset) + except Exception: # noqa: BLE001 + return None + def reference_curve( self, profile_name: str, n: int = REFERENCE_PROFILE_CURVE_POINTS ) -> JSONDict | None: @@ -5107,6 +5729,215 @@ class ProfileStore: except Exception: # pragma: no cover - defensive; never break the sensor return [] + def resolve_profile_duration(self, profile_name: str) -> float | None: + """A profile's expected duration the way the MATCHER resolves it, or None. + + `_build_match_snapshots` takes the first usable of three sources, and a + caller that checks fewer of them disagrees with the candidate pool about + which profiles exist and how long they are: + + 1. ``profile["avg_duration"]`` - the rolling average. + 2. the sample cycle's own ``duration``. + 3. the wall-clock span of that cycle's stored trace. + + Source 3 is the weaker cousin of the builder's third fallback, which uses + the RESAMPLED segment's span (and therefore the longest gap-free run). The + raw span is used here because resampling is too expensive for a caller on + the event loop, and it errs the same way item 251 does: toward believing a + profile is usable, never toward hiding one. + + Never raises; returns ``None`` when no source yields a positive duration. + """ + try: + profiles = self._data.get("profiles") or {} + profile = profiles.get(profile_name) if isinstance(profiles, dict) else None + if not isinstance(profile, dict): + return None + + def _positive(value: Any) -> float | None: + try: + number = float(value or 0.0) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(number) or number <= 0: + return None + return number + + direct = _positive(profile.get("avg_duration")) + if direct is not None: + return direct + + sample_id = profile.get("sample_cycle_id") + # find_stored_cycle returns (cycle, origin), not the cycle. + sample = self.find_stored_cycle(sample_id)[0] if sample_id else None + if sample is None: + for candidate in self.iter_evidence_cycles(): + if candidate.get("profile_name") == profile_name and candidate.get( + "power_data" + ): + sample = candidate + break + if sample is None: + return None + + stored = _positive(sample.get("duration")) + if stored is not None: + return stored + + points = decompress_power_data(cast(Any, sample)) + if len(points) < 2: + return None + return _positive(points[-1][0] - points[0][0]) + except Exception: # noqa: BLE001 - a duration lookup must never raise + return None + + def compute_profile_terminal_signature( + self, profile_name: str + ) -> dict[str, Any] | None: + """How this program ends, measured from its OWN cycles. Pure statistics. + + A dishwasher's cycle does not end at its last wash activity: it goes quiet + for a passive drying phase and then, usually, emits a short low-power + terminal event (the final pump-out) before standby. Measured across the + real corpus, that quiet-plus-event span is a median 11% of the cycle and + reaches 43%, so the phase is substantial cycle content rather than a tail. + + Returns ``None`` when the profile has fewer than + ``TERMINAL_SIGNATURE_MIN_CYCLES`` evidence cycles to measure, else:: + + quiet_before_s median seconds of quiet before the terminal event + event_seconds median duration of the event itself + event_watts median peak power of the event + event_watts_frac that peak as a fraction of the cycle's own peak + position_frac where the event sits in the cycle (0-1) + seen_in / measured how many of the measured cycles showed one. + ``measured`` counts the cycles the statistic could + be computed on at all, so a degenerate trace (too + few points, all-zero, zero span) is excluded + rather than counted as a cycle that did not do it + consistency seen_in / measured, 0-1 + + **Read `consistency` before trusting the rest.** The same appliance emits + the event only sometimes: on the best-sampled device in the corpus (5-10 s + sampling, 1100+ samples through the quiet phase) it appeared in roughly 6 + of 15 runs of one program, the others reaching the same duration with + nothing above 0.6 W. So the event's PRESENCE is informative and its + ABSENCE is not, which is why this is reported rather than acted on - see + register item 238 for the two end-detection rules this evidence rejected. + + The detection threshold scales with each cycle's own peak + (``TERMINAL_EVENT_PEAK_FRAC``) instead of a fixed wattage, because a + fixed one does not survive the range involved: measured terminal events + are a median 1.3% of their cycle's peak, and the #399 spin extractor's + 400 W ceiling finds nothing at all on a dishwasher whose pump-out is 33 W. + + The bound of scaling that way: an appliance whose standby is a large + FRACTION of its own peak has no quiet phase to find. A 2000 W dishwasher + idling at 2 W is 0.1% and reads as quiet; a 60 W pump idling at 0.5 W is + 0.8% and does not. That is the honest answer for such a device (its + standby is not distinguishable from its running load at this resolution), + so it reports ``seen_in: 0`` rather than a fabricated event. + + Never raises; returns ``None`` on any error. + """ + try: + cycles = [ + c + for c in self.iter_evidence_cycles() + if c.get("profile_name") == profile_name and c.get("power_data") + ] + if len(cycles) < TERMINAL_SIGNATURE_MIN_CYCLES: + return None + + quiet: list[float] = [] + seconds: list[float] = [] + watts: list[float] = [] + watt_fracs: list[float] = [] + positions: list[float] = [] + measured = 0 + + for cycle in cycles: + points = decompress_power_data(cast(Any, cycle)) + if len(points) < 5: + continue + peak = max(p for _t, p in points) + if peak <= 0: + continue + threshold = peak * TERMINAL_EVENT_PEAK_FRAC + span = points[-1][0] - points[0][0] + if span <= 0: + continue + # Counted only once the trace is measurable AT ALL. An all-zero + # cycle (a stored cycle that is nothing but 0.0 W keepalives, + # item 260) or a zero-span one can never produce an event, so + # putting it in the denominator would report the appliance as + # less consistent for a reason that is not about the appliance. + # Every `continue` BELOW this line is a real measured-but-absent + # cycle and must stay counted: that is what `consistency` means. + measured += 1 + + # Walk back to the last run above the threshold, then to the start + # of that run, then to the end of the quiet stretch before it. + end_i = None + for i in range(len(points) - 1, -1, -1): + if points[i][1] > threshold: + end_i = i + break + if end_i is None: + continue + start_i = end_i + while start_i > 0 and points[start_i - 1][1] > threshold: + start_i -= 1 + if start_i == 0: + continue # the run reaches the start: no quiet phase before it + # The quiet PHASE, not the sample interval: walk back to the + # previous activity. Measuring the gap to the preceding sample + # would just report the plug's reporting rate, since a plug that + # keeps sampling through the drying phase has no large interval. + prev_i = None + for i in range(start_i - 1, -1, -1): + if points[i][1] > threshold: + prev_i = i + break + if prev_i is None: + continue # nothing before it: this is the main activity + gap = points[start_i][0] - points[prev_i][0] + if gap < TERMINAL_QUIET_MIN_S: + continue # the event is part of the main activity, not after it + + quiet.append(gap) + seconds.append(points[end_i][0] - points[start_i][0]) + event_peak = max(p for _t, p in points[start_i : end_i + 1]) + watts.append(event_peak) + watt_fracs.append(event_peak / peak) + positions.append((points[start_i][0] - points[0][0]) / span) + + if not measured: + return None + if not quiet: + return { + "quiet_before_s": None, + "event_seconds": None, + "event_watts": None, + "event_watts_frac": None, + "position_frac": None, + "seen_in": 0, + "measured": measured, + "consistency": 0.0, + } + return { + "quiet_before_s": round(float(np.median(quiet)), 1), + "event_seconds": round(float(np.median(seconds)), 1), + "event_watts": round(float(np.median(watts)), 2), + "event_watts_frac": round(float(np.median(watt_fracs)), 4), + "position_frac": round(float(np.median(positions)), 4), + "seen_in": len(quiet), + "measured": measured, + "consistency": round(len(quiet) / measured, 3), + } + except Exception: # noqa: BLE001 - a statistic must never break the panel + return None + def compute_envelope_conformance( self, profile_name: str, @@ -5399,8 +6230,16 @@ class ProfileStore: self, current_power_data: list[tuple[str, float]] | list[tuple[datetime, float]] | list[tuple[float, float]] | list[list[float]], current_duration: float, + in_progress: bool = False, ) -> MatchResult: - """Run profile matching asynchronously in executor.""" + """Run profile matching asynchronously in executor. + + ``in_progress`` marks a live, still-running cycle, which makes Stage 4 + compare the elapsed stretch against the same stretch of each candidate + instead of against its complete duration/energy (#400). Defaults to False + so the final match at cycle end - where the cycle IS complete - and every + other caller keep their existing behaviour. + """ # 1. Prepare data in main thread (Access ProfileStore state safely) group_members: dict[str, list[str]] = {} member_snaps: dict[str, dict[str, Any]] = {} @@ -5437,7 +6276,7 @@ class ProfileStore: if not segments: return MatchResult(None, 0.0, 0.0, None, [], False, 0.0) current_seg = max(segments, key=lambda s: len(s.power)) - if len(current_seg.power) < 12: + if len(current_seg.power) < MATCH_MIN_RESAMPLED_POINTS: return MatchResult(None, 0.0, 0.0, None, [], False, 0.0) current_power_list = current_seg.power.tolist() @@ -5588,8 +6427,9 @@ class ProfileStore: "; ".join(skipped_profiles) ) - # Stage 5: collapse cohesive near-duplicate groups into one aggregate - # candidate each (loose groups stay individual). No-op without groups. + # Stage 5: map cohesive near-duplicate groups to their members (loose + # groups stay individual). The members are scored individually and the + # family is collapsed after the worker. No-op without groups. snapshots, group_members, member_snaps = self._grouped_snapshots(snapshots) config = { @@ -5597,6 +6437,7 @@ class ProfileStore: "max_duration_ratio": self._max_duration_ratio, "dtw_bandwidth": self.dtw_bandwidth, "energy_mode": self.energy_mode, + "in_progress": bool(in_progress), # On-device tuned scoring weights (opt-in); empty = shipped defaults. **self._matching_overrides(), } @@ -5628,6 +6469,18 @@ class ProfileStore: ) return MatchResult(None, 0.0, 0.0, None, [], False, 0.0, [], {}, is_confident_mismatch=True, mismatch_reason="all_rejected") + # Stage 5, part 1 (#400): the members of a cohesive group were scored on + # their own curves; collapse each family to its best-scoring member now. + # Must happen BEFORE the ambiguity check - two members of the same family + # sitting next to each other in the ranking is precisely what grouping + # exists to stop reading as an ambiguous top-2. + # Captured BEFORE the collapse: every member was scored on its own curve, + # and the collapse drops all but the best sibling. This is the only place + # the selected member's own blended score still exists (see + # MatchResult.label_confidence). + scored_by_name = {str(c.get("name")): c for c in candidates} + candidates = collapse_group_candidates(candidates, group_members) + best = candidates[0] # Reconstruct MatchResult. Top-level ambiguity (safeguard #1): a close @@ -5637,20 +6490,47 @@ class ProfileStore: best_name = best["name"] best_duration = best["profile_duration"] + member_confidence: float | None = None # Stage 5: if a group won, pick the best-fitting member. if best_name in group_members: chosen, member_fit, member_dur = self._stage5_pick_member( - current_power_list, current_duration, group_members[best_name], member_snaps + current_power_list, current_duration, group_members[best_name], member_snaps, + in_progress=in_progress, ) best_name = chosen if member_dur: best_duration = member_dur + # The selected member's OWN blended pipeline score, for the label gate. + # None when the member has no candidate record at all: `member_snaps` + # holds every present member, including ones `compute_matches_worker` + # rejected at the Stage-1 duration gate, so Stage 5 can select a member + # the matcher itself refused to consider (measured: 2 of 267 group wins). + _chosen_cand = scored_by_name.get(chosen) + if _chosen_cand is not None: + try: + member_confidence = float(_chosen_cand["score"]) + except (TypeError, ValueError, KeyError): + member_confidence = None # Safeguard #2: the group aggregate matched but if the chosen member # does not individually fit reasonably (vs the group score), the real # program may be a different single profile -> treat as uncertain. # member_fit is a Stage-2-only score; best["score"] includes DTW-blend + - # duration/energy agreement (typically 25-30% higher). Use 0.55× to avoid - # the threshold being effectively too strict for DTW-boosted groups. + # duration/energy agreement. Use 0.55× to avoid the threshold being + # effectively too strict for DTW-boosted groups. + # + # This is a coarse "is the member nowhere near" backstop, NOT a + # mislabel detector, and it must not be recalibrated into one. Measured + # on the real corpus (267 group wins), member_fit/best["score"] has + # median 0.877 whole-cycle and 1.018 mid-cycle - so the ratio is not a + # stable ~0.75 as the "25-30% higher" note used to claim, it straddles + # 1.0 and reaches 2.07 mid-cycle (member_fit warps the FULL trace + # against the FULL template, while mid-cycle Stage 2 scored a prefix + # pair). Worse, on the folds where Stage 5 picked the WRONG member the + # ratio is *higher* than where it picked the right one (median 0.992 vs + # 0.947), so no constant K separates them: sweeping K from 0.55 to 1.00 + # cost up to 53 correct labels to prevent 12 wrong ones. The label gate + # uses MatchResult.label_confidence instead, which compares two + # like-for-like blended scores. if member_fit is not None and best["score"] > 0 and member_fit < 0.55 * best["score"]: is_ambiguous = True # Overrun guard: if the cycle has already outlasted the chosen member's @@ -5658,8 +6538,34 @@ class ProfileStore: # to ambiguous so Smart Termination falls back to the power timeout. if best_duration and current_duration > best_duration * 1.05: is_ambiguous = True - # Relabel the winning candidate for the ranking / diagnostics. - candidates = [{**best, "name": best_name, "profile_duration": best_duration}, *candidates[1:]] + # Relabel the winning candidate for the ranking / diagnostics, and carry + # the Stage-5 provenance with it. `score` deliberately stays the group's + # (i.e. the best-scoring sibling's) blended score: that is the number the + # matcher decided on, and every live consumer - Smart Termination's + # match_confidence_threshold, the ML end-guard's defer_finish_confidence, + # the unmatch/divergence tracking, the ETA blend - is calibrated against + # it. Lowering it here would silently re-tune end detection. + # + # What the cohesion gate does NOT make close is this blended score. It + # bounds the *shape* spread (measured sibling Stage-2 spread: median + # 0.068 whole-cycle), but Stage 4's duration/energy agreement exists + # precisely to pull temperature/spin siblings apart, so the blended + # spread is 2.3x wider (median 0.154, max 0.431). Hence + # `stage5_member_score`: the selected member's own blended score, which + # MatchResult.label_confidence gates labelling on. The ranking and the + # live_match training snapshots therefore record all three numbers and + # never imply the chosen member earned its sibling's score. + candidates = [ + { + **best, + "name": best_name, + "profile_duration": best_duration, + "stage5_member_fit": member_fit, + "stage5_member_score": member_confidence, + "stage5_group": best["name"], + }, + *candidates[1:], + ] matched_phase = None if best_name: @@ -5689,6 +6595,7 @@ class ProfileStore: ranking=candidates[:5], # populate ranking (consumed for training snapshots) is_prefix_ambiguous=is_prefix_ambiguous, is_prefix_ambiguous_full_shape=full_shape_hit, + member_confidence=member_confidence, ) async def async_verify_alignment( @@ -6104,18 +7011,24 @@ class ProfileStore: self._data["active_cycle"] = None self._data["last_active_save"] = None # Newer persisted state must also be wiped, else a "wipe all" leaves trained - # models, groups, matcher tuning, histories, and counters behind. + # models, groups, matcher tuning and histories behind. The two lifetime + # odometers are the documented exception - see the note below. self._data["custom_phases"] = [] self._data["ml_model_versions"] = {} self._data["profile_groups"] = {} self._data["maintenance_log"] = [] + self._data.pop("armed_program", None) self._data["playground_presets"] = {} self._data["matching_config"] = {} self._data["match_ranking_history"] = [] self._data["ml_last_training_run"] = None self._data["ml_training_history"] = {} - self._data["lifetime_energy_wh"] = 0.0 - self._data["lifetime_cycle_count"] = 0 + # The two lifetime odometers deliberately SURVIVE a wipe (#414). They record + # what the appliance has done, not what WashData has stored: the cycle count + # now drives the maintenance reminders (and any external "every N cycles" + # task), and the energy total backs a TOTAL_INCREASING Energy-dashboard + # sensor where a silent reset reads as a meter replacement. Both remain + # correctable by hand - the panel's Maintenance tab edits the cycle count. self._data["settings_changelog"] = [] self._data["suggestion_apply_cycle_count"] = 0 self._cached_sample_segments = {} @@ -6640,6 +7553,12 @@ class ProfileStore: ) self._data = data_dict self._cached_sample_segments = {} + # Re-apply the odometer floor: an import can add or replace past_cycles + # after async_load already healed it, and export_data copies the STORED + # value rather than the getter's floored one - so an export taken before + # the next restart could report a lifetime count below its own retained + # history. Only ever increases, and idempotent. + self._heal_lifetime_cycle_count() await self.async_save() return { @@ -6995,6 +7914,12 @@ class ProfileStore: await self.async_rebuild_envelope(p) self._cached_sample_segments = {} + # Re-apply the odometer floor: an import can add or replace past_cycles + # after async_load already healed it, and export_data copies the STORED + # value rather than the getter's floored one - so an export taken before + # the next restart could report a lifetime count below its own retained + # history. Only ever increases, and idempotent. + self._heal_lifetime_cycle_count() await self.async_save() settings_out: dict[str, Any] = {} diff --git a/custom_components/ha_washdata/progress.py b/custom_components/ha_washdata/progress.py index 1132438d..2eaa1bb7 100644 --- a/custom_components/ha_washdata/progress.py +++ b/custom_components/ha_washdata/progress.py @@ -794,12 +794,28 @@ def projected_energy( price: float | None, end_expectation_fn: EndExpFn, logger: logging.Logger | None = None, + cost_so_far: float | None = None, + cost_so_far_wh: float | None = None, ) -> tuple[float | None, float | None]: """Project total energy (Wh) and cost for the running cycle. Prefers the on-device ``total_energy`` regressor; otherwise falls back to ``energy_so_far / progress_fraction``. Returns ``(wh, cost)``; both values are ``None`` when progress is too low or there is no energy yet. Never raises. + + ``cost_so_far`` is the dynamic-tariff cost already incurred (#426): the energy + consumed so far, charged at the price in force when it was consumed. When it + is given, only the *remaining* energy is charged at the current price, so a + cycle that ran through a cheap window is not retroactively repriced at the + expensive one it happens to be in now. The future half is still the current + price - forecasting the tariff is deliberately out of scope. + + ``cost_so_far_wh`` is the energy ``cost_so_far`` was charged for, which is NOT + ``energy_so_far``: the cost integrates the power trace while ``energy_so_far`` + is the detector's per-reading accumulator, and the two count outages and + sub-threshold intervals differently. Subtracting the wrong one leaves the + overlap double-charged or uncharged. Defaults to ``energy_so_far`` so a caller + that has only the cost keeps the previous behaviour. """ logger = logger or _LOGGER try: @@ -820,7 +836,19 @@ def projected_energy( price_val = float(price) except (TypeError, ValueError): price_val = None - cost = (projected_wh / 1000.0) * price_val if price_val is not None else None + if price_val is None: + cost = None + elif cost_so_far is None: + cost = (projected_wh / 1000.0) * price_val + else: + charged_wh = energy_so_far + if cost_so_far_wh is not None: + try: + charged_wh = float(cost_so_far_wh) + except (TypeError, ValueError): + charged_wh = energy_so_far + remaining_wh = max(0.0, projected_wh - charged_wh) + cost = float(cost_so_far) + (remaining_wh / 1000.0) * price_val return projected_wh, cost except Exception: # noqa: BLE001 - projection must never break estimates return None, None diff --git a/custom_components/ha_washdata/select.py b/custom_components/ha_washdata/select.py index dbb91db1..b3adf65a 100644 --- a/custom_components/ha_washdata/select.py +++ b/custom_components/ha_washdata/select.py @@ -104,9 +104,14 @@ class WashDataProgramSelect(SelectEntity): current = self._manager.current_program manual_active = getattr(self._manager, "manual_program_active", False) + armed = getattr(self._manager, "armed_program", None) if manual_active and current: self._attr_current_option = current + elif armed and armed in self._attr_options: + # Pinned for the next cycle while the appliance is idle (#411): show the + # choice back, or the select would snap to Auto the moment it was set. + self._attr_current_option = armed elif current in ("detecting...", "off", "restored..."): self._attr_current_option = OPTION_AUTO elif current: diff --git a/custom_components/ha_washdata/sensor.py b/custom_components/ha_washdata/sensor.py index 63c7d3ee..1c3e9290 100644 --- a/custom_components/ha_washdata/sensor.py +++ b/custom_components/ha_washdata/sensor.py @@ -305,6 +305,13 @@ class WasherStateSensor(WasherBaseSensor): if anomaly and anomaly != "none": attrs["cycle_anomaly"] = anomaly attrs["overrun_ratio"] = round(self._manager.overrun_ratio, 2) + # Envelope position (visible only): where the run maps onto the matched + # profile's own curve, which is independent of elapsed time and therefore + # still meaningful when a cycle over- or under-runs its usual duration. + # Refreshed only during the low-power phases the alignment runs in. + envelope_position = self._manager.envelope_position + if envelope_position is not None: + attrs["envelope_position"] = envelope_position # Post-cycle anomaly data (underrun, energy spike/low) from last completed cycle. last_post = self._manager.last_cycle_post_anomaly if isinstance(last_post, dict): @@ -977,7 +984,15 @@ class PumpRunsTodaySensor(WasherBaseSensor): class WasherCycleCountSensor(WasherBaseSensor): - """Sensor reporting the total number of completed cycles stored for this device.""" + """Odometer: how many cycles this appliance has run, ever. + + Reports the monotonic lifetime counter, not ``len(stored history)`` (#414). The + stored-history number is capped at ``max_past_cycles`` and shrinks when the user + deletes a record, so as a state it was unusable for the thing people build on it: + an "every N cycles" maintenance schedule, whether WashData's own reminders or an + external integration's. The old number is still available as the + ``stored_cycles`` attribute. + """ def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: self.entity_description = SensorEntityDescription( @@ -985,12 +1000,25 @@ class WasherCycleCountSensor(WasherBaseSensor): translation_key="cycle_count", icon="mdi:counter", native_unit_of_measurement="cycles", + # TOTAL, not TOTAL_INCREASING, even though the odometer only rises on its + # own. The user can correct it downward (WS set_lifetime_cycle_count -> + # set_lifetime_cycle_count(force=True)), and TOTAL_INCREASING reads any + # decrease as a meter reset: correcting 500 down to 300 makes the + # long-term sum absorb the new reading whole, recording 300 cycles that + # were never run. TOTAL applies the -200 the correction actually means. + # No last_reset: the sum accumulates continuously. + state_class=SensorStateClass.TOTAL, ) super().__init__(manager, entry) @property def native_value(self) -> int: # type: ignore[override] - return self._manager.cycle_count + return self._manager.lifetime_cycle_count + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: # type: ignore[override] + """Expose the retained-history count the state used to report.""" + return {"stored_cycles": self._manager.cycle_count} class WasherEnergyTotalSensor(WasherBaseSensor): diff --git a/custom_components/ha_washdata/signal_processing.py b/custom_components/ha_washdata/signal_processing.py index d4ced4f2..9f44eb03 100644 --- a/custom_components/ha_washdata/signal_processing.py +++ b/custom_components/ha_washdata/signal_processing.py @@ -21,7 +21,10 @@ Constraint: All computations must be dt-aware (robust to irregular cadence). Constraint: Resampling must be segment-based (no interpolation across gaps). """ +from __future__ import annotations + from dataclasses import dataclass +from collections.abc import Sequence from typing import List, Tuple import numpy as np @@ -97,6 +100,195 @@ def integrate_wh( +# ─── Time-weighted energy cost (#426) ───────────────────────────────────────── +# +# A dynamic tariff moves while the appliance runs, so the single price in force +# when the cycle ended is not the price the energy was bought at. These helpers +# integrate the power trace against a piecewise-constant price timeline instead. +# +# Pure and hass-free on purpose: the manager (live cycles), the recorder backfill +# (historic cycles) and the tests all go through the same math, so a cycle costed +# live and the same cycle recosted from recorder history cannot disagree. + + +def compact_price_timeline( + points: Sequence[Tuple[float, float]], + *, + max_points: int = 240, + decimals: int = 6, +) -> List[Tuple[float, float]]: + """Normalize a ``(offset_s, price)`` timeline for storage. + + Sorts by offset, rounds prices, drops entries that repeat the previous price + (a tariff that did not change costs nothing to record), and coarsens to at + most ``max_points`` by keeping the entries that introduce the largest price + steps - the points that matter least to the integral go first, so the capped + timeline stays close to the uncapped one instead of being truncated at an + arbitrary time. + """ + cleaned: List[Tuple[float, float]] = [] + for entry in points or []: + try: + offset = float(entry[0]) + price = round(float(entry[1]), decimals) + except (TypeError, ValueError, IndexError, OverflowError): + # OverflowError: json keeps an oversized integer literal as an unbounded + # int, and float() on one of those raises rather than returning inf. An + # imported cycle's price_timeline reaches here unvalidated, and this + # function's contract is to drop a malformed entry, not to raise. + continue + if not np.isfinite(offset) or not np.isfinite(price): + # nan AND +-inf: "inf" parses out of a sensor state like any other + # float, and an infinite price makes every downstream cost infinite. + continue + cleaned.append((offset, price)) + if not cleaned: + return [] + cleaned.sort(key=lambda item: item[0]) + + deduped: List[Tuple[float, float]] = [] + for offset, price in cleaned: + if deduped and deduped[-1][1] == price: + continue + deduped.append((offset, price)) + + if max_points > 0 and len(deduped) > max_points: + # Rank the entries by the size of the price step each one introduces and keep + # the largest. Index 0 is never a candidate - it anchors the price in force at + # cycle start. Ranked in one pass rather than removing the smallest step + # repeatedly: this also runs on the event loop (the in-memory bound in + # ``manager._append_price_sample``), and the quadratic version is what a + # pathologically chatty price entity would pay for. + ranked = sorted( + range(1, len(deduped)), + key=lambda i: abs(deduped[i][1] - deduped[i - 1][1]), + reverse=True, + ) + keep = set(ranked[: max_points - 1]) + keep.add(0) + deduped = [deduped[i] for i in sorted(keep)] + return deduped + + +def integrate_wh_by_price( + timestamps: np.ndarray, + power: np.ndarray, + price_points: Sequence[Tuple[float, float]], + *, + max_gap_s: float | None = None, +) -> List[Tuple[float, float]]: + """Split a trace's energy across a piecewise-constant price timeline. + + Returns ``[(price, wh), ...]``, one entry per price point, in timeline order. + + Each trapezoid interval is charged whole to the price in force at its + *midpoint* rather than being split at the price boundary. That keeps the sum + of the returned Wh **exactly** equal to :func:`integrate_wh` over the same + inputs, which is what lets the caller apportion an external meter reading + across the segments without the two figures drifting apart. Splitting would + also break the ``max_gap_s`` outage mask: a boundary inserted inside a gap + would turn one excluded interval into two short included ones. The error is + bounded by a single sample interval per price change - seconds against a + tariff that steps hourly. + """ + try: + prices = [float(p) for _, p in price_points or []] + offsets = np.asarray([float(o) for o, _ in price_points or []], dtype=float) + except (TypeError, ValueError, OverflowError): + # Same JSON boundary as compact_price_timeline above, which already drops + # an entry for exactly these reasons: an imported or hand-edited + # price_timeline keeps an oversized integer literal as an unbounded int, + # and float() on one raises rather than returning inf. Every in-repo + # caller compacts first, so this only binds a direct caller - and for one + # of those "unusable timeline" means no timeline, which is the empty + # return cycle_cost below already falls back on. Returning a zero Wh per + # price instead would claim the timeline was fine and the trace carried no + # energy, a different statement that only lands on the same fallback by + # accident. + return [] + if not prices: + return [] + ts = np.asarray(timestamps, dtype=float) + pw = np.asarray(power, dtype=float) + if ts.size < 2 or pw.size != ts.size: + return [(price, 0.0) for price in prices] + + dt_hours = np.diff(ts) / 3600.0 + avg_power = (pw[:-1] + pw[1:]) * 0.5 + energy = avg_power * dt_hours + if max_gap_s is not None: + mask = (dt_hours > 0) & (dt_hours <= float(max_gap_s) / 3600.0) + energy = np.where(mask, energy, 0.0) + + midpoints = (ts[:-1] + ts[1:]) * 0.5 + # side="right" - 1 gives the last price point at or before the midpoint. + # Clipped at 0 so a trace that starts before the first price point is charged + # at that first price rather than dropped. + idx = np.clip(np.searchsorted(offsets, midpoints, side="right") - 1, 0, len(prices) - 1) + totals = np.bincount(idx, weights=energy, minlength=len(prices)) + return [(prices[i], float(totals[i])) for i in range(len(prices))] + + +def cycle_cost( + timestamps: np.ndarray, + power: np.ndarray, + price_points: Sequence[Tuple[float, float]], + *, + max_gap_s: float | None = None, + report_wh: float | None = None, +) -> Tuple[float, float] | None: + """Time-weighted cost of a cycle, and the effective price per kWh it implies. + + ``report_wh`` is the user-facing energy figure when it differs from the + integral - i.e. an external meter's start->end delta (issue #316). The + segments are scaled so they sum to it, which apportions the meter's total by + the shape of the power trace and guarantees ``cost == report_wh/1000 * + effective_price``. Without that, the cost shown next to a kWh figure would be + computed from a different amount of energy than the kWh figure itself. + + Returns ``None`` when the trace carries no energy to charge for, or when + ``report_wh`` is given but is not a usable positive figure, so the caller can + fall back to the single-price behaviour instead of reporting a false zero or a + cost that describes a different amount of energy than the kWh beside it. + """ + segments = integrate_wh_by_price(timestamps, power, price_points, max_gap_s=max_gap_s) + if not segments: + return None + integrated = sum(wh for _, wh in segments) + if integrated <= 0: + return None + scale = 1.0 + if report_wh is not None: + try: + report = float(report_wh) + # OverflowError alongside the rest, as in compact_price_timeline above: an + # imported or hand-edited record keeps an oversized integer literal as an + # unbounded int, and float() on one raises instead of returning inf. This + # function's contract is to hand the decision back, not to raise. + except (TypeError, ValueError, OverflowError): + return None + if not np.isfinite(report) or report <= 0: + # The caller asked for the cost of *this* figure. Costing the trace + # integral instead would print a price beside a 0 kWh readout, so hand + # the decision back rather than answer a question that was not asked. + return None + scale = report / integrated + cost = sum(wh * scale / 1000.0 * price for price, wh in segments) + # Finite inputs can still leave the finite range: a price near the float + # ceiling against a multi-kWh trace overflows to inf, and a subnormal + # `report_wh` (5e-324 passes every check above) makes `scale` underflow, so + # the charged energy rounds to exactly 0.0 and the division raises. Both end + # in a non-finite or undefined cost, which is what the caller's fallback is + # for, so hand the decision back rather than publish one. + charged_kwh = integrated * scale / 1000.0 + if not np.isfinite(cost) or charged_kwh <= 0.0: + return None + effective_price = cost / charged_kwh + if not np.isfinite(effective_price): + return None + return cost, effective_price + + def resample_uniform( timestamps: np.ndarray, power: np.ndarray, dt_s: float = 5.0, gap_s: float = 60.0 ) -> List[Segment]: diff --git a/custom_components/ha_washdata/store.py b/custom_components/ha_washdata/store.py index 3e6a4c0d..2c23ed2c 100644 --- a/custom_components/ha_washdata/store.py +++ b/custom_components/ha_washdata/store.py @@ -201,12 +201,12 @@ class StoreBridge: # ── catalog browse (reads) ─────────────────────────────────────────────────── - async def list_brands(self, query: str | None = None, include_pending: bool = True) -> list[dict[str, Any]]: + async def list_brands(self, query: str | None = None, *, include_pending: bool = True) -> list[dict[str, Any]]: return await self._client.list_brands(query, include_pending=include_pending) async def search_devices( self, brand: str | None, appliance_type: str | None, - model_query: str | None = None, include_pending: bool = False, + model_query: str | None = None, *, include_pending: bool = False, ) -> list[dict[str, Any]]: return await self._client.search_devices( brand, appliance_type, model_query=model_query, include_pending=include_pending, @@ -228,8 +228,11 @@ class StoreBridge: self._client.refresh_catalog() return {"ok": True} - async def get_profiles(self, device_id: str) -> list[dict[str, Any]]: - return await self._client.get_profiles(device_id) + async def get_profiles(self, device_id: str, *, include_pending: bool = True) -> list[dict[str, Any]]: + """Shared programs for a catalog appliance. Pending-inclusive, like the device + list this is opened from and like get_cycles below; passed explicitly so the + browse cannot silently drift back to approved-only (which showed nothing).""" + return await self._client.get_profiles(device_id, include_pending=include_pending) async def device_profiles(self, brand: str, model: str, appliance_type: str) -> dict[str, Any]: """Profiles for the appliance identified by brand/model/type (for the Share diff --git a/custom_components/ha_washdata/store_client.py b/custom_components/ha_washdata/store_client.py index 23eeea21..a1833f58 100644 --- a/custom_components/ha_washdata/store_client.py +++ b/custom_components/ha_washdata/store_client.py @@ -503,6 +503,22 @@ class StoreClient: return None return self._approved_only(cached) + def _serve_from_type_superset( + self, appliance_type: str | None, include_pending: bool, page_size: int + ) -> list[dict[str, Any]] | None: + """A warm brand-unscoped device list for ``appliance_type``, if one can answer a + brand-scoped request for that type. Prefers the pending-inclusive entry (which + contains the approved rows too) and narrows it in memory when the caller wants + approved only. Returns None when nothing usable is cached. + """ + base = f"devices::{appliance_type or ''}" + for pend in ((1,) if include_pending else (1, 0)): + cached = self._cache_get(f"{base}:{pend}:{page_size}") + if cached is None or len(cached) >= page_size: + continue + return cached if include_pending else self._approved_only(cached) + return None + async def search_devices( self, brand: str | None = None, appliance_type: str | None = None, model_query: str | None = None, *, include_pending: bool = False, page_size: int = 500, @@ -526,6 +542,18 @@ class StoreClient: if shared is not None: return _finish(shared) + # A brand-unscoped list for the same appliance type is a strict superset of every + # brand-scoped list of that type, so once the panel's model search has warmed it + # (it queries by model prefix across all brands) every subsequent brand browse of + # that type is answered in memory instead of costing its own query. Only shares + # downwards, and only from an untruncated entry -- same two rules as + # _serve_from_superset, whose include_pending downgrade this reuses. + if brand: + wide = self._serve_from_type_superset(appliance_type, include_pending, page_size) + if wide is not None: + bl = brand.lower() + return _finish([r for r in wide if str(r.get("brand_lc", "")) == bl]) + def _build() -> dict[str, Any]: filters = [self._status_filter(include_pending)] if appliance_type: @@ -739,7 +767,18 @@ class StoreClient: """count + average of a reference cycle's 5-star ratings (info only).""" return await self._rating_agg(f"cycles/{_seg(cycle_id)}") - async def get_profiles(self, dev_id: str, include_pending: bool = False, page_size: int = 100) -> list[dict[str, Any]]: + async def get_profiles(self, dev_id: str, include_pending: bool = True, page_size: int = 100) -> list[dict[str, Any]]: + """Shared programs for one catalog appliance, most-recent-first. + + ``include_pending`` defaults to **True**, matching ``get_cycles`` and the + device browse. It used to default to False, which made the Store tab list a + device with a "Programs: N" chip and then report "No shared programs for this + appliance yet": approval is a community vote that almost nothing has passed + (measured on the live catalog: the IKEA TALLBODA dishwasher has 9 profiles, + **0** of them approved), so an approved-only query on a pending-inclusive + device list is empty for practically every appliance. Pending rows are + publicly readable and the panel tags them "awaiting approval". + """ sq = { "from": [{"collectionId": "profiles"}], "where": self._where([ diff --git a/custom_components/ha_washdata/suggestion_engine.py b/custom_components/ha_washdata/suggestion_engine.py index 97676e53..17ce4b39 100644 --- a/custom_components/ha_washdata/suggestion_engine.py +++ b/custom_components/ha_washdata/suggestion_engine.py @@ -59,6 +59,7 @@ from .const import ( DEFAULT_ANTI_WRINKLE_MAX_POWER, CONF_DEVICE_TYPE, CONF_PUMP_STUCK_DURATION, + CONF_MATCH_PERSISTENCE, DEVICE_TYPE_DRYER, DEVICE_TYPE_PUMP, DEVICE_TYPE_WASHING_MACHINE, @@ -68,7 +69,11 @@ from .const import ( DEFAULT_MIN_OFF_GAP_BY_DEVICE, DEFAULT_MIN_OFF_GAP, DEFAULT_SAMPLING_INTERVAL, + DEFAULT_MATCH_PERSISTENCE, + MATCH_INTERVAL_SUGGESTION_DECISION_FRAC, + MATCH_INTERVAL_SUGGESTION_MIN_S, ) +from .options_utils import option_int from .time_utils import power_data_to_offsets # ─── Clean-cycle selection ──────────────────────────────────────────────────── @@ -852,16 +857,126 @@ class SuggestionEngine: } # 4. Profile Match Interval - suggested_match = int(max(10, median_dt * 10)) + # + # Cadence alone is the wrong yardstick (#431): the interval is spent + # waiting to IDENTIFY a program, so what bounds it is how long the + # shortest program runs, not how chatty the plug is. A 60 s reporting + # plug produced 599 s, and with match_persistence 3 no profile could then + # settle before ~30 min - on a 43-minute program that is most of the run, + # and the value is worse than the 300 s default the user started from. + # + # So cap the DECISION budget (interval x persistence), not the interval + # alone, at MATCH_INTERVAL_SUGGESTION_DECISION_FRAC of the shortest known + # profile. Capping the interval alone would let a higher persistence + # reintroduce the same wait. + suggested_match = int(max(MATCH_INTERVAL_SUGGESTION_MIN_S, median_dt * 10)) + reason_match = f"Based on observed update cadence (median={median_dt:.1f}s) * 10." + reason_match_key = "suggestion.reason.match_interval" + reason_match_params: dict[str, Any] = {"median": f"{median_dt:.1f}"} + + shortest_profile_s = self._shortest_profile_duration() + if shortest_profile_s is not None: + # option_int holds the whole guard (register items 278, 279): it proves + # the value is float-representable before returning it, so the divisor + # below cannot raise, and the floor of 1 is shared with the manager's + # read of the same key instead of being re-derived here. + persistence = option_int( + _op_opts.get(CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE), + DEFAULT_MATCH_PERSISTENCE, + minimum=1, + ) + cap = ( + shortest_profile_s * MATCH_INTERVAL_SUGGESTION_DECISION_FRAC + ) / persistence + if cap < suggested_match: + # Floor at MATCH_INTERVAL_SUGGESTION_MIN_S to match the uncapped + # branch: a very short profile must not drive the matcher into a + # per-second poll. + suggested_match = int(max(MATCH_INTERVAL_SUGGESTION_MIN_S, cap)) + pct = MATCH_INTERVAL_SUGGESTION_DECISION_FRAC * 100.0 + if cap < MATCH_INTERVAL_SUGGESTION_MIN_S: + # The floor won, so the budget rule does NOT hold here. Say that + # instead of claiming a bound that was not applied: the reason is + # shown to the user beside the value they are asked to accept. + budget = MATCH_INTERVAL_SUGGESTION_MIN_S * persistence + reason_match = ( + f"The shortest program ({shortest_profile_s:.0f}s) alone would cap " + f"this at {cap:.0f}s, below the {MATCH_INTERVAL_SUGGESTION_MIN_S}s " + f"minimum, so it is held there: {persistence} consecutive matches " + f"take {budget}s, which is more than {pct:.0f}% of that program. " + f"Matching only runs when a reading arrives (median=" + f"{median_dt:.1f}s), so the minimum costs nothing." + ) + reason_match_key = "suggestion.reason.match_interval_floored" + reason_match_params = { + "median": f"{median_dt:.1f}", + "shortest": f"{shortest_profile_s:.0f}", + "persistence": str(persistence), + "pct": f"{pct:.0f}", + "cap": f"{cap:.0f}", + "minimum": str(MATCH_INTERVAL_SUGGESTION_MIN_S), + "budget": str(budget), + } + else: + reason_match = ( + f"Capped so {persistence} consecutive matches fit in " + f"{pct:.0f}% of the shortest program ({shortest_profile_s:.0f}s); " + f"the update cadence (median={median_dt:.1f}s) alone would " + f"have suggested a longer interval." + ) + reason_match_key = "suggestion.reason.match_interval_capped" + reason_match_params = { + "median": f"{median_dt:.1f}", + "shortest": f"{shortest_profile_s:.0f}", + "persistence": str(persistence), + "pct": f"{pct:.0f}", + } + suggestions[CONF_PROFILE_MATCH_INTERVAL] = { "value": suggested_match, - "reason": f"Based on observed update cadence (median={median_dt:.1f}s) * 10.", - "reason_key": "suggestion.reason.match_interval", - "reason_params": {"median": f"{median_dt:.1f}"}, + "reason": reason_match, + "reason_key": reason_match_key, + "reason_params": reason_match_params, } return suggestions + def _shortest_profile_duration(self) -> float | None: + """Shortest learned ``avg_duration`` across all profiles, or None. + + Mirrors the ``avg > 60`` guard the model-suggestion generator already + applies, so a hand-created profile with a placeholder duration cannot + collapse a suggestion to its floor. Returns None when nothing usable is + known yet - the caller then leaves its cadence-only value untouched, so a + fresh install behaves exactly as before. + """ + try: + # Snapshot, not the live map: get_profiles() hands back + # self._data["profiles"] itself, and this runs in an executor thread + # while the event loop can label, rename or delete a profile - which + # would raise "dictionary changed size during iteration" and abort the + # whole suggestion pass. + profiles = dict(self.profile_store.get_profiles() or {}) + except Exception: # pylint: disable=broad-exception-caught + return None + if not isinstance(profiles, dict): + return None + shortest: float | None = None + for name, prof in profiles.items(): + if not isinstance(prof, dict): + continue + # The matcher's own contract, not just `avg_duration`: a profile whose + # length comes from its sample cycle is still matchable, and if it is + # the shortest one, capping against a longer program would overrun the + # decision budget for it. The helper absorbs the malformed cases + # (including the unbounded-int OverflowError of register item 194). + avg = self.profile_store.resolve_profile_duration(name) + if avg is None or avg <= 60.0: + continue + if shortest is None or avg < shortest: + shortest = avg + return shortest + def generate_model_suggestions(self) -> dict[str, Any]: """Generate suggestions for model parameters based on past cycles.""" suggestions: dict[str, dict[str, Any]] = {} diff --git a/custom_components/ha_washdata/translations/panel/bg.json b/custom_components/ha_washdata/translations/panel/bg.json index 03cb5109..22caf38c 100644 --- a/custom_components/ha_washdata/translations/panel/bg.json +++ b/custom_components/ha_washdata/translations/panel/bg.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Близо до края тази програма утихва за около {mins} мин, след което консумира около {watts} Вт за {secs} сек, преди да завърши. Наблюдавано при {seen}/{measured} измерени цикъла: уредът не го прави при всяко пускане.", + "quiet_tail": "~{mins} мин тишина накрая", "artifact_tip": "{n} открити аномалии (напр. отворена врата по средата на цикъла) – отворете, за да ги видите на графиката", "auto": "(автоматично разпознато)", "built_in_tag": "вградена", @@ -77,11 +79,14 @@ "approved": "Одобрено", "awaiting": "Изчаква одобрение", "imported_tip": "Импортирано от магазина на общността. Използва се само за съпоставяне, не се брои в статистиката.", + "unmatchable": "не може да се съпостави", "not_importable": "неприложимо", "exists": "съществува", - "backfilled_tip": "Открито в импортирана история на мощността. Влияе само на съпоставянето на програми, не се брои в статистиката." + "backfilled_tip": "Открито в импортирана история на мощността. Влияе само на съпоставянето на програми, не се брои в статистиката.", + "armed": "(важи за следващия цикъл)" }, "btn": { + "reset_zoom": "Нулиране на мащаба", "set_brand_model": "Задаване на марка и модел", "refresh_catalog": "Опресняване на каталога", "add_device": "+ Добавяне на устройство", @@ -217,6 +222,8 @@ "trim": "Изрязване", "trim_split": "Изрязване / Разделяне", "undo": "Отмяна", + "unlabel_selected": "Премахване на етикета от избраните ({n})", + "unlabel_selected_tip": "Премахва етикета на профила от избраните цикли. Те остават в историята ви и могат да бъдат етикетирани отново по-късно - само спират да оформят този профил.", "use": "Използвайте", "wipe_all": "Изтриване на всички данни", "delete_selected": "Изтриване на избраните ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Приложи най-доброто", "pg_apply_to_settings": "Запазване в настройките", "pg_apply_to_settings_tip": "Копиране на стойностите, редактирани тук, в активните настройки на това устройство", - "search": "Търсене", "import": "Импортиране", "disconnect": "Прекъсване на връзката", "connect_github": "Свързване с GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Прочитане от Home Assistant", "hist_scan": "Търсене на цикли", "hist_import_n": "Импортиране на цикли ({n})", - "hist_goto_cycles": "Покажи циклите" + "hist_goto_cycles": "Покажи циклите", + "save_total_cycles": "Запазване на общия брой" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Поправете конфликтите преди запазване", "attn_title": "Конфликти в настройките: {n}", + "price_entity": { + "power_sensor": "Това е Сензорът за мощност на уреда, а не цена. Обект за цена има предимство пред статичната цена по-долу, така че циклите биха се остойностявали по ватове за kWh - изчистете това поле.", + "energy_sensor": "Това е Измервателният уред за енергия на уреда, а не цена. Обект за цена има предимство пред статичната цена по-долу, така че циклите биха се остойностявали по показанието на уреда за kWh - изчистете това поле.", + "device_class": "Класът на този сензор е {dc}, а не цена за kWh. Обект за цена има предимство пред статичната цена по-долу - изчистете това поле или го насочете към сензор за тарифа.", + "unit": "Този сензор отчита в {unit}, което не е цена за kWh. Обект за цена има предимство пред статичната цена по-долу - изчистете това поле или го насочете към сензор за тарифа." + }, "settings_banner": "Конфликти в настройките: {n}. Проверете маркираните раздели и ги поправете преди запазване.", "settings_banner_btn": "Към първия", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Източник на настройките", "phase_catalog": "Фазов каталог", "playground": "Тестова среда", - "process_history": "История на процеса", + "process_history": "Обработка на историята", "settings_history": "История на настройките", "status": "Статус", "status_graph": "Графика на състоянието", @@ -360,12 +373,12 @@ "wipe_history": "Изтриване на историята", "recommendations": "Препоръки ({n})", "pg_detection_params": "Настройки за откриване", - "pg_timeline": "Хронология на събитията", "pg_outcome": "Резултат от симулацията", "pg_across_cycles": "За всички ваши цикли", "community_store": "Магазин на общността", "online_account": "Магазин на общността и онлайн функции", - "import_power_history": "Импортиране на историята на мощността" + "import_power_history": "Импортиране на историята на мощността", + "service_status": "Статус на сервиза" }, "health": { "fair": "Приемливо качество на съвпадение", @@ -373,6 +386,10 @@ "poor": "⚠ Лошо качество на съвпадение" }, "lbl": { + "envelope_position_tip": "Позиция върху записаната крива на съпоставената програма, {pct}%. Измерва се чрез подравняване на този цикъл спрямо профила, а не чрез броене на времето, затова остава вярна, когато едно пускане е по-дълго или по-кратко от обичайното. Обновява се, докато уредът е тих, затова може да изостава по време на активна фаза.", + "envelope_position": "крива {pct}%", + "aria_playground_chart3": "Интерактивна графика на мощността на цикъла: посочете, за да видите време/мощност, превъртете или използвайте два пръста за мащабиране, плъзнете за преместване", + "device_unresolved": "Недостъпно устройство ({id})", "drag_to_resize": "Плъзнете за преоразмеряване", "actions": "Действия", "activity": "активност", @@ -509,7 +526,7 @@ "profile_index": "индекс на профила →", "profile_name": "Име на профил", "program": "Програма", - "program_tip": "Замества кой профил се съпоставя с текущия цикъл. Автоматичното разпознаване позволява на интеграцията да избере най-доброто съвпадение автоматично. Закачете конкретна програма, за да я принудите, когато авт. разпознаването греши или знаете какво работи.", + "program_tip": "Замества кой профил се съпоставя с текущия цикъл. Автоматичното разпознаване позволява на интеграцията да избере най-доброто съвпадение автоматично. Закачете конкретна програма, за да я принудите, когато авт. разпознаването греши или знаете какво работи. Изберете програма, преди да пуснете уреда, и тя ще се приложи веднага щом започне следващият цикъл.", "progress": "Напредък", "raw_socket": "Суров сигнал от контакта", "read_only": "Само за четене", @@ -535,7 +552,7 @@ "settings_basic": "Основни", "settings_detail_level": "Ниво на детайлност на настройките", "settings_overrides": "Замествания на настройки", - "show_debug": "Показване на карта за отстраняване на грешки в мача на живо на страницата за състояние (увереност, неяснота, най-добри кандидати)", + "show_debug": "Показване на карта за отстраняване на грешки при съвпадението на живо на страницата за състояние (увереност, неяснота, най-добри кандидати)", "show_expected": "Показване на очакваното наслагване на крива (съвпадащ профил, оранжево)", "show_raw": "Показване на превключвателя за необработени данни от контакта в живия графикон на мощността", "sparkline": "Тенденция на продължителността на последните цикли", @@ -544,6 +561,7 @@ "stage4": "Етап 4 – съответствие", "status": "Статус", "tail_trim": "Изрязване в края (с)", + "tap_to_dismiss": "докоснете графиката, за да затворите", "timer_auto_pause": "Автоматична пауза", "timer_min": "мин", "timer_msg_placeholder": "Съобщение (по избор, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Оставащо време (модел)", "pg_time_left_model_tip": "Оценка на оставащото време от модела (оценител на фази + ML), а не статичен обратен брояч", "phase": "Фаза", - "pg_mode_simulate": "Симулация", "pg_mode_history": "Тест върху история", - "pg_mode_sweep": "Претърсване", "pg_ev_detected": "Открит", "pg_ev_match": "Съвпадението е прието", "pg_ev_match_changed": "Съвпадението е променено", @@ -612,14 +628,11 @@ "pg_sweep_param": "Параметър", "pg_objective": "Цел", "pg_steps": "Стъпки", - "pg_sweep_2d": "2D топлинна карта", - "pg_sweep_param_y": "2-ри параметър", "pg_obj_match": "Точност на съвпаденията", "pg_obj_endtiming": "Точност на завършването", "pg_obj_falseend": "Честота на фалшиви завършвания", "pg_obj_overrun": "Отклонение в продължителността", "pg_obj_ambiguity": "Честота на двусмисленост", - "pg_best_combo": "най-добра комбинация", "pg_mode_optimize": "Оптимизиране", "pg_group_matching": "Съпоставяне на програми", "pg_zoom_hint": "двойно щракване нулира мащаба", @@ -706,7 +719,12 @@ "task_history_import_apply": "Импортиране на цикли", "evidence_real_cycles": "Цикли, изпълнени от тази машина", "evidence_reference_cycles": "Изтеглени от магазина на общността", - "evidence_backfill_cycles": "Намерени в импортирана история на мощността" + "evidence_backfill_cycles": "Намерени в импортирана история на мощността", + "zoom_hint_touch": "мащабирайте с два пръста", + "zoom_hint_touch_zoomed": "мащабирайте с два пръста · двойно докосване за нулиране", + "cycles_since_service": "{since} / {total} цикъла", + "total_cycles_run": "цикъла общо", + "correct_total_cycles": "Коригиране на общия брой" }, "log": { "all_levels": "Всички нива", @@ -756,7 +774,7 @@ "merge_cycles": "Обединяване на {n} цикли", "new_group": "Нова профилна група", "new_phase": "Нова фаза", - "process_history_msg": "Повторно стартирайте съпоставяне, обновете предложенията, обучете отново ML (ако е активирано) и преизчислете качеството на цикъла за всички съхранени цикли. Това може да отнеме известно време.", + "process_history_msg": "Повторно стартирайте съпоставянето, обновете предложенията, обучете отново ML (ако е активирано), преизчислете разходите спрямо записаните цени на енергията и преизчислете качеството на циклите за всички съхранени цикли. Това може да отнеме известно време.", "process_history_ok": "Обработи", "process_history_title": "Обработка на историята", "process_recording": "Запис на процеса", @@ -789,6 +807,7 @@ "history_import": "Импортиране на историята на мощността" }, "msg": { + "pg_canvas_empty3": "Изберете цикъл по-горе и натиснете „Стартиране“, за да го симулирате. След това посочете, за да видите стойностите, превъртете или използвайте два пръста за мащабиране и плъзнете за преместване.", "tail_trim_hint": "Премахнете толкова секунди от края", "store_sibling_hint": "Няма нищо споделено за точния ви модел? Близък модел от същата марка обикновено е добра отправна точка.", "store_declare_appliance": "Кажете на WashData кой уред притежавате и този раздел ще показва конфигурациите, които други хора са споделили за него. Можете също да въведете марка по-горе, за да разгледате каталога.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Надвиши обичайния диапазон на мощността за ~{n} сек.", "auto_label_intro": "Присвояване на профили към немаркирани цикли, чиято степен на достоверност на съвпадението надвишава прага.", "automations_intro": "WashData задейства събития {start} / {end} и предоставя обекти, затова известията и действията най-добре се изграждат като обикновени автоматизации на Home Assistant. По-долу са показани автоматизациите, използващи това устройство.", - "cleanup_intro": "Всеки етикетиран цикъл е насложен. Отбележете отклоненията и изтрийте, за да почистите профила.", + "cleanup_intro": "Всеки етикетиран цикъл е насложен. Отбележете отклоненията и след това премахнете етикета им (остават в историята, но вече не оформят този профил) или ги изтрийте окончателно.", "clear_debug_hint": "Премахнете съхранените данни за отстраняване на грешки, за да освободите място.", "collecting_data": "Събиране на данни. Цикли, необходими преди да започне фината настройка: {need} ({current}/{min}).", "compare_overlay_profiles": "Профили на наслагване (бледи)", @@ -902,7 +921,7 @@ "preferences_adv": "Предпочитания", "prefs_personal": "Те се отнасят само за вашия акаунт в Home Assistant.", "font_size_hint": "Направете всичко в този панел по-голямо или по-малко. Прилага се за вашия акаунт на това устройство.", - "process_history_hint": "Повторно стартирайте съпоставяне за всички съхранени цикли, обновете предложенията за настройка, обучете отново ML моделите (ако е активирано) и преизчислете качеството на цикъла. Стартирайте след пакетни прегледи.", + "process_history_hint": "Повторно стартирайте съпоставянето за всички съхранени цикли, обновете предложенията за настройка, обучете отново ML моделите (ако е активирано), преизчислете разходите спрямо записаните цени на енергията и преизчислете качеството на циклите. Стартирайте това след пакет от прегледи.", "profile_deleted": "Профилът е изтрит", "profile_poor_health_detail": "Циклите, присвоени на този профил, имат непоследователни форми или ниска степен на доверие. Помислете за повторно изграждане на обвивката или преглед на циклите с етикети.", "profiles_intro": "Щракнете върху профил за статистика, фази и почистване. Групирайте почти идентични програми (еднаква форма/продължителност, различна температура или центрофуга), така че съвпадението надеждно избира между тях.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "Профилът '{name}' има ниска оценка на съответствие - последните му цикли варират значително или се разпознават слабо. Прегледайте циклите му или запишете профила отново, за да останат точни разпознаването и оценките на времето.", "advisory_shape_drift": "Профилът '{name}' се е отклонил значително от първоначалната си форма на консумация на мощност. Уредът може да е променил поведението си с времето (напр. котлен камък, износване). Обмислете повторно записване на този профил с последните цикли.", "advisory_shape_drift_corr": "Профилът '{name}' се е отклонил значително от първоначалната си форма на консумация на мощност (корелация {corr}). Уредът може да е променил поведението си с времето (напр. котлен камък, износване). Обмислете повторно записване на този профил с последните цикли.", + "advisory_unmatchable": "Профилът '{name}' никога няма да бъде съпоставен: зад него няма нито един цикъл с данни за мощността, така че WashData няма с какво да сравни текущия цикъл. Маркирайте цикъл с тази програма (или запишете такъв), за да го върнете в съпоставянето.", + "advisory_unmatchable_no_duration": "Профилът '{name}' никога няма да бъде съпоставен: има цикъл с данни за мощността, но без използваема продължителност, така че WashData няма еталонна дължина, спрямо която да мери съпоставянето. Запишете тази програма отново или задайте очакваната ѝ продължителност, за да я върнете в съпоставянето.", "pg_no_alerts": "В това изпълнение не са открити проблеми.", "pg_override_active": "Използват се вашите редактирани настройки вместо текущите", - "pg_history_intro": "Пусни отново последните си цикли през реалния детектор и модула за съвпадение. Първо променете настройка за откриване в „Симулация“, за да видите тук сравнение преди/след.", "pg_history_empty": "Натиснете „Изпълни“, за да възпроизведете циклите си.", "pg_history_summary": "{detected}/{total} открити · {correct} съвпаднали правилно", "pg_sweep_no_metric": "Няма достатъчно данни за оценка на тази цел.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Сканирането е неуспешно.", "hist_scan_expired": "Това сканиране вече не е налично. Моля, сканирайте отново.", "hist_import_failed": "Импортирането е неуспешно.", - "imported_history_readonly": "Открито в импортирана история на мощността. Влияе на съпоставянето на програми, но не се брои във вашата статистика и не може да се изрязва или разделя. Използвайте „Етикет“, за да посочите програмата." + "imported_history_readonly": "Открито в импортирана история на мощността. Влияе на съпоставянето на програми, но не се брои във вашата статистика и не може да се изрязва или разделя. Използвайте „Етикет“, за да посочите програмата.", + "odometer_intro": "Този общ брой само расте. Изтриването на запис за цикъл не го променя и той продължава да се увеличава и след ограничението за съхранявана история, така че напомнянията за сервиз остават точни.", + "odometer_correct_hint": "Задайте това, ако е записан цикъл, който никога не се е изпълнявал, или ако уредът вече е изпълнявал цикли преди инсталирането на WashData.", + "no_reminders_set": "Все още няма зададени напомняния за сервиз.", + "toast_program_armed": "Програмата е зададена за следващия цикъл: {program}" }, "phase_desc": { "anti_crease": "Случайни кратки движения след завършване за намаляване на бръчките.", @@ -1161,6 +1185,10 @@ "doc": "След като цикълът надхвърли очакваната си продължителност, колко дълго съдомиялната машина трябва да остане неактивна (под «Праг на спиране»), преди WashData да спре да чака финално източване и да завърши цикъла. Увеличете го, ако вашата машина има дълга тиха фаза на сушене преди късно финално източване, което се пропуска - по-широк прозорец позволява на научената продължителност да следва сезонните промени (по-студена входяща вода = по-дълги цикли), вместо да се фиксира към старата средна стойност. То само съкращава изчакването спрямо вътрешния 30-минутен лимит за изчакване на финалния пик (източване на вода), но никога не го удължава.", "label": "Тихо завършване на пасивното сушене" }, + "anti_crease_finalize_ratio": { + "doc": "Колко навътре в очакваната продължителност на съпоставената програма трябва да е навлязъл цикълът, преди WashData да може да прецени, че наблюдаваното въртене на барабана при ниска мощност е опашката против бръчки, и да завърши цикъла там. Очакваната продължителност е средната за програмата, затова при сушилня, чието време на сушене с датчик за влажност зависи от товара, повечето пускания никога не достигат 98% от нея и опашката виси чак до резервния таймаут - често 15 до 40 минути след като машината реално е спряла. При такава сушилня намалете стойността (напр. 0.75). При перални оставете стойността по подразбиране: пералнята прекарва по-голямата част от цикъла под Макс. антибръчкова мощност, така че този дял е единственото, което отделя тиха фаза в средата на прането от опашката, и намаляването му може да съкрати прането наполовина. Оставете празно за стойността по подразбиране (0.98). Това е различна настройка от Коефициент на интелигентно прекратяване, която управлява отделна проверка за предсрочно завършване.", + "label": "Коефициент на завършване на режима против бръчки" + }, "anti_wrinkle_enabled": { "doc": "Разпознайте кратките импулси на сушене с ниска мощност, които сушилнята излъчва след основната топлинна фаза, и ги запазете прикрепени към завършения цикъл, вместо да ги четете като нови цикли.", "label": "Активиране на антибръчково засичане" @@ -1230,7 +1258,7 @@ "label": "Брой повторения при край" }, "energy_price_entity": { - "doc": "Сензор с текущата цена на електроенергията за kWh (напр. динамична тарифа). Има предимство пред статичната цена по-долу. Всеки цикъл замразява цената в сила, когато приключи.", + "doc": "Сензор с текущата цена на електроенергията за kWh (например динамична тарифа). Трябва да е цена, а не собственият сензор за мощност или енергия на контакта: има предимство пред статичната цена по-долу, така че брояч на kWh тук остойностява всеки цикъл по показанието на брояча вместо по вашата тарифа. Когато „Цена, претеглена по време“ е включена, всеки цикъл се таксува по цената, валидна във всеки момент от работата му; в противен случай върху него се фиксира цената, валидна в момента на приключването му.", "label": "Обект на цена на енергия" }, "energy_price_static": { @@ -1258,7 +1286,7 @@ "label": "Инвертиране на zewn. тригер (задействане при ИЗКЛ.)" }, "learning_confidence": { - "doc": "Ако резултатът за съвпадение попада между това и доверието на автоматичното етикетиране, известие за обратна връзка ви моли да потвърдите идентифицираната програма. Под този резултат мачът е твърде несигурен, за да излезе на повърхността. Трябва да се поддържа под сигурността на автоматичното етикетиране.", + "doc": "Ако резултатът за съвпадение е между тази стойност и „Доверие за автоматично маркиране“, известие за обратна връзка ви моли да потвърдите разпознатата програма. Под този резултат съвпадението е твърде несигурно, за да бъде показано. Трябва да се поддържа под „Доверие за автоматично маркиране“.", "label": "Доверие в обучението" }, "linked_device": { @@ -1326,7 +1354,7 @@ "label": "Задействане на HA събитие за известия" }, "notify_icon": { - "doc": "Незадължителна mdi икона за известието (напр. mdi:washing-machine). Оставете празно за платформата по подразбиране.", + "doc": "Малката икона, показвана в известието (напр. mdi:washing-machine). Android я изчертава в лентата на състоянието; iOS я показва вместо иконата на приложението и изисква придружаващото приложение 2026.8 или по-ново. Оставете празно за иконата по подразбиране на платформата.", "label": "Икона на известие" }, "notify_live_chronometer": { @@ -1426,7 +1454,7 @@ "label": "Праг на съвпадение" }, "profile_unmatch_threshold": { - "doc": "Ако мач на живо в средата на цикъла падне под този резултат, предварителната идентификация се изчиства. Дръжте го малко под прага на съвпадение, така че краткото понижаване на сходството да не преобърне дисплея обратно към несравнимо.", + "doc": "Ако съвпадението на живо в средата на цикъла падне под този резултат, предварителното разпознаване се изчиства. Дръжте го малко под „Праг на съвпадение“, за да не може кратък спад в сходството да върне показанието обратно към „без съвпадение“.", "label": "Праг на несъвпадение" }, "progress_reset_delay": { @@ -1437,10 +1465,6 @@ "doc": "Секунди помпата може да работи непрекъснато, преди да бъде маркирана като евентуално блокирана (задейства събитието блокирана помпа).", "label": "Продължителност при заседнала помпа" }, - "running_dead_zone": { - "doc": "След като цикъл започне, спадовете на мощността в този прозорец се игнорират. Пералните машини се пълнят със студена вода (спад близо до 0 W преди загряване) - без тази защита тази фаза на пълнене изглежда като край на цикъла. Това НЕ пропуска данни: пълната мощност се записва от T=0. Машината за предложения измерва действителния модел на стартиране на вашата машина и го оразмерява автоматично.", - "label": "Мъртва зона при работа" - }, "sampling_interval": { "doc": "Очаквано време между показанията на сензора - използва се за оразмеряване на прозореца за изглаждане и правилно стартиране на отстраняването на отскок. Всяка актуализация на сензора се улавя независимо от тази стойност; той само калибрира изчисленията надолу по веригата. Механизмът за предложения измерва действителния каданс на вашия сензор от минали цикли и го задава автоматично.", "label": "Интервал на вземане на проби" @@ -1568,13 +1592,17 @@ "label": "Интервал на профила на мощността", "doc": "Размер на кофата за атрибута на сензора power_profile за всеки профил (плоски средни стойности в Вт за слот, използвани от външни планировчици като EMHASS и tibber_prices). По-малките кофи запазват кратките пикове на мощността остри; по-голямата кофа изглажда формата. По подразбиране 15 мин. Само за четене; не засяга откриването." }, + "notify_live_silent": { + "label": "Тихи актуализации на живо", + "doc": "Само за iOS. Опреснявайте напредъка на живо безшумно: всяка актуализация идва без звук и вибрация, с по-нисък приоритет, който пести батерия и който iOS може за кратко да групира. Първата актуализация в цикъла (тази, която стартира Live Activity), както и известията за начало и край, продължават да сигнализират както обикновено. Изключете това, за да получавате сигнал при всяка актуализация." + }, "notify_live_sticky": { "label": "Запазване на живото известие при докосване", "doc": "Само за Android. Направете известието за ход на живо постоянно (закачено), за да не го откажете при докосване. Изключено запазва поведението по подразбиране, при което докосването го откажва." }, "notify_live_click_action": { - "label": "Цел при докосване на живото известие", - "doc": "Само за Android. Накъде отваря докосването на известието за ход на живо (напр. /lovelace/laundry или пълен URL адрес), вместо началната страница на приложението. Оставете празно за поведението по подразбиране." + "label": "Цел при докосване на известието", + "doc": "Накъде отваря докосването на известие от този уред, на Android и iOS. Празно отваря панела WashData за този уред. Въведете път към табло (напр. /lovelace/laundry) или пълен URL адрес, за да го насочите другаде, или „none“ за никаква цел при докосване." }, "door_opens_at_end": { "label": "Вратата се отваря автоматично в края", @@ -1587,6 +1615,14 @@ "profile_evidence_sources": { "label": "Цикли, които оформят програмата", "doc": "Кои цикли се използват за изграждане на кривата на мощността на всяка програма и за съпоставяне на завършен цикъл с нея. Ако махнете отметката на даден вид, той спира да оформя програмите ви, без нищо да се изтрива - циклите остават в списъка Цикли и все още могат да бъдат маркирани или премахнати. Полезно, ако не се доверявате на импортираните данни. Статистиката не се влияе: в нея винаги се броят само циклите, които тази машина наистина е изпълнила. Махането на всички отметки се игнорира, защото програма без нито един цикъл никога не би могла да съвпадне." + }, + "energy_price_dynamic": { + "label": "Цена, претеглена по време", + "doc": "Таксува всеки цикъл по цената, валидна във всеки момент от работата му, вместо по единствената цена в момента на приключването му. Работи само с обект за цена (статичната цена не се променя). Не изисква допълнителна настройка: цената се следи на живо, докато цикълът върви, а „Обработка на историята“ в Диагностика преизчислява разходите на по-старите цикли от recorder. Изключено = класичното поведение, една цена, фиксирана накрая." + }, + "curve_preroll_seconds": { + "doc": "Колко секунди отчитания от прекъснати опити за стартиране WashData може да пренесе в началото на кривата на цикъла. Кривата на цикъла обикновено започва от опита за стартиране, който в крайна сметка е успял, затова при машина, която опитва няколко пъти, преди да потегли - избор на програма, заключване на вратата, първо пълнене - в началото на всяка крива липсват първите минути реална работа. Задайте стойност малко над най-дългия такъв разбег на вашата машина (300 с стигат с излишък за повечето). 0 изключва функцията. Имайте предвид, че включването ѝ измества записаното начало по-рано, така че циклите, записани преди и след промяната, носят различна продължителност за една и съща програма; научените средни стойности ще се изместват, докато по-старите цикли не отпаднат от историята.", + "label": "Предзапис на кривата" } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Разделяне…", "deleting": "Изтриване…", "imported": "Импортирано", - "preparing": "Подготовка…" + "preparing": "Подготовка…", + "unlabelling": "Премахване на етикета…" }, "tab": { "advanced": "Разширено", @@ -1764,7 +1801,7 @@ "panel_settings_saved": "Настройките на панела са запазени", "pg_preset_loaded": "Конфигурацията „{name}“ е заредена", "pg_preset_saved": "Конфигурацията „{name}“ е запазена", - "pg_sugg_loaded": "Заредени {n} предложени стойности - стартирайте тестовата среда за сравнение", + "pg_sugg_loaded": "Заредени {n} предложени стойности - стартирайте тестовата среда за сравнение", "pg_sugg_ml_loaded": "Заредени {n} ML-калибрирани стойности - стартирайте тестовата среда за сравнение", "phase_name_required": "Изисква се име на фаза", "phase_updated": "Фазата е актуализирана", @@ -1778,6 +1815,7 @@ "recording_stopped": "Записът спря", "review_saved": "Прегледът е запазен", "settings_conflicts": "Поправете маркираните конфликти в настройките преди запазване.", + "settings_no_changes": "Няма промени за запазване", "settings_reverted": "Настройките са възстановени; презареждане на интеграцията", "settings_saved": "Настройките са запазени; презареждане на интеграцията", "suggestions_applied": "Приложени предложения; презареждане на интеграцията", @@ -1836,7 +1874,11 @@ "export_selective_done": "Експортът е изтеглен", "import_selective_done": "Импортирани са {profiles} профил(а) и {cycles} цикъл(а)", "hist_csv_required": "Първо заредете CSV файл или поставете съдържанието му", - "file_read_failed": "Файлът не можа да бъде прочетен" + "file_read_failed": "Файлът не можа да бъде прочетен", + "unlabel_done": "Премахнат етикет от цикли: {count}", + "unlabel_failed": "Премахването на етикета е неуспешно: {error}", + "odometer_saved": "Общият брой цикли е запазен", + "odometer_save_failed": "Общият брой не може да бъде запазен: {error}" }, "suggestion": { "both_agree": "WashData препоръчва", @@ -1883,10 +1925,6 @@ "higher": "Потвърждава само съвпадения с висока увереност; повече цикли остават без етикет", "lower": "Съпоставя повече цикли; някои може да са леко грешни" }, - "running_dead_zone": { - "higher": "Игнорира кратките скокове на мощност в режим на готовност – по-малко чувствителен към шум", - "lower": "Реагира на по-кратки изблици на мощност – улавя бързи преходни активности" - }, "start_threshold_w": { "higher": "Избягва фалшиви стартове от кратковременни скокове на мощност", "lower": "Улавя програми с ниска мощност; може да се задейства от смущения" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} чисти цикъла ({pct}%) са имали вътрешна пауза над 60s, която е продължила; изисквай {val} потвърждение(я) за край.", "learning_confidence": "Достоверност p05 на {cycles} цикъла с потребителски етикети ({p05}); под тази стойност изисквай проверка.", "match_interval": "Въз основа на наблюдаваната честота на обновяване (медиана={median}s) * 10.", + "match_interval_capped": "Ограничено така, че {persistence} последователни съвпадения да се поберат в {pct}% от най-кратката програма ({shortest}s); самата честота на обновяване (медиана={median}s) би подсказала по-дълъг интервал.", + "match_interval_floored": "Само най-кратката програма ({shortest}s) би ограничила стойността до {cap}s, под минимума от {minimum}s, затова тя се задържа на минимума: поредица от {persistence} последователни съпоставяния отнема {budget}s, което е повече от {pct}% от тази програма. Съпоставянето се извършва само когато пристигне ново отчитане (медиана={median}s), така че минимумът не добавя натоварване.", "max_duration_ratio": "Въз основа на продължителността на етикетираните цикли (p95={p95}).", "min_active": "Въз основа на минималната работна мощност ({min}W), наблюдавана в последния цикъл.", "min_duration_ratio": "Задържано възможно най-ниско, за да се разпознае програмата рано в цикъла; праговете на достоверност и нееднозначност предотвратяват преждевременни потвърждения.", @@ -1973,6 +2013,7 @@ "start_duration_threshold": "Секунди над прага за потвърждаване на старта", "start_threshold_w": "Минимални ватове, за да се счита за стартирал", "stop_threshold_w": "Под тази стойност машината се счита за изключена", + "anti_crease_finalize_ratio": "Дял от очакваната продължителност на съпоставената програма, който цикълът трябва да достигне, преди опашката от въртене против бръчки да може да бъде завършена; намалете го при сушилни с дълга опашка от въртене, при перални оставете стойността по подразбиране", "anti_wrinkle_enabled": "Поглъщайте импулсите на въртене на барабана след основната фаза, вместо да ги четете като нови цикли", "anti_wrinkle_exit_power": "Мощността между импулсите трябва да падне под това, за да остане активен режимът против бръчки", "anti_wrinkle_idle_timeout": "Разрешено спокойствие между два импулса на въртене на барабана, преди режимът против бръчки да приключи", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Етап 3: разстояние на полунасищане на DDTW; по-малко = по-чувствително към форма (по подразбиране 30)", "dtw_refine_top_n": "Етап 3: кандидати, преоценени от DTW; увеличете до 7-9, ако правилният профил заема 4.-5. място (по подразбиране 5)", "duration_scale": "Етап 4: лог-съотношение, при което съответствието на продължителността се намалява наполовина; по-малко = по-строга санкция (по подразбиране 0,175)", - "energy_scale": "Етап 4: лог-съотношение, при което съответствието на енергията се намалява наполовина; по-малко = по-строга санкция (по подразбиране 0,25)" + "energy_scale": "Етап 4: лог-съотношение, при което съответствието на енергията се намалява наполовина; по-малко = по-строга санкция (по подразбиране 0,25)", + "curve_preroll_seconds": "Секунди отчитания от прекъснати опити за стартиране, пренасяни в началото на кривата на цикъла; 0 = изключено" }, "col": { "profile_tip": "Име на съпоставената програма. „Без етикет“ означава, че в края на цикъла не е съвпаднал нито един профил.", @@ -1998,10 +2040,11 @@ "date_tip": "Дата и час на започване на цикъла.", "duration_tip": "Общо време на изпълнение на цикъла от началото до края.", "energy_tip": "Общо консумирана енергия (kWh). Изчислява се чрез интегриране на мощността във времето.", - "cost_tip": "Цена на енергията за този цикъл, фиксирана при завършване по действащата тогава цена (енергия × цена за kWh). Задайте цена в Настройки, за да се попълни.", + "cost_tip": "Разход за енергия за този цикъл, фиксиран при приключването му. Когато „Цена, претеглена по време“ е включена, кривата на мощността се таксува по цената, валидна във всеки момент; в противен случай енергия x единствената цена, валидна накрая. Задайте цена в Настройки, за да се попълни.", "confidence_tip": "Увереност на съвпадението на профила (0–100%). Колко точно кривата на мощността на цикъла съвпада с определената програма.", "health_tip": "ML здраве на цикъла (по-високо = по-добре). Щракнете върху цикъл, за да го инспектирате и прегледате.", - "flags_tip": "Маркери на общността и индикатори за качество на данните" + "flags_tip": "Маркери на общността и индикатори за качество на данните", + "cost_dynamic_tip": "Претеглено по време: таксувано по цената, валидна по време на цикъла (ефективно {price} {cur}/kWh)." }, "stat": { "avg": "средно", @@ -2028,9 +2071,9 @@ "finished": "Цикълът достигна крайно състояние и приключи." }, "store": { + "search_ph2": "Търсене по марка или модел…", "your_model_tip": "Това е уредът, който сте задали в настройките", "your_model": "Ваш", - "search_brand_ph": "Търсене по марка…", "programs_count": "Програми: {n}", "browse": "Разглеждане", "device": "Уред", @@ -2075,7 +2118,8 @@ "golden": "Повторна обработка: попълване на референтни", "suggestions": "Повторна обработка: предложения", "ml_training": "Повторна обработка: ML обучение", - "health": "Повторна обработка: здраве на циклите" + "health": "Повторна обработка: здраве на циклите", + "costs": "Повторна обработка: разходи за енергия" }, "pg_sweep": { "optimize": "Оптимизиране: {param}" diff --git a/custom_components/ha_washdata/translations/panel/bs.json b/custom_components/ha_washdata/translations/panel/bs.json index 910fc8f5..d09bbc0f 100644 --- a/custom_components/ha_washdata/translations/panel/bs.json +++ b/custom_components/ha_washdata/translations/panel/bs.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Pri kraju ovaj program utihne na otprilike {mins} min, zatim povuče oko {watts} W tokom {secs} s i završava. Uočeno u {seen}/{measured} izmjerenih ciklusa: uređaj to ne radi pri svakom pokretanju.", + "quiet_tail": "~{mins} min tišine na kraju", "artifact_tip": "Otkrivene su {n} anomalije (npr. vrata otvorena sredinom ciklusa) – otvorite da ih vidite na grafikonu", "auto": "(automatski otkriveno)", "built_in_tag": "ugrađen", @@ -77,11 +79,14 @@ "approved": "Odobreno", "awaiting": "Čeka odobrenje", "imported_tip": "Uvezeno iz zajedničke trgovine. Koristi se samo za podudaranje, ne broji se u statistiku.", + "unmatchable": "ne može se podudariti", "not_importable": "nije dostupno", "exists": "postoji", - "backfilled_tip": "Otkriveno u uvezenoj historiji snage. Utiče samo na podudaranje programa, ne broji se u statistiku." + "backfilled_tip": "Otkriveno u uvezenoj historiji snage. Utiče samo na podudaranje programa, ne broji se u statistiku.", + "armed": "(primjenjuje se na sljedeći ciklus)" }, "btn": { + "reset_zoom": "Poništi zumiranje", "set_brand_model": "Postavi marku i model", "refresh_catalog": "Osvježi katalog", "add_device": "+ Dodaj uređaj", @@ -217,6 +222,8 @@ "trim": "Skrati", "trim_split": "Skrati / Podijeli", "undo": "Poništi", + "unlabel_selected": "Ukloni oznaku s odabranih ({n})", + "unlabel_selected_tip": "Uklanja oznaku profila s odabranih ciklusa. Ostaju u vašoj historiji i mogu se kasnije ponovo označiti - samo prestaju formirati ovaj profil.", "use": "Koristi", "wipe_all": "Obriši sve podatke", "delete_selected": "Izbriši odabrano ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Primijeni najbolje", "pg_apply_to_settings": "Sačuvaj u postavke", "pg_apply_to_settings_tip": "Kopiraj ovdje uređene vrijednosti u aktivne postavke ovog uređaja", - "search": "Pretraži", "import": "Uvezi", "disconnect": "Prekini vezu", "connect_github": "Poveži GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Pročitaj iz Home Assistant", "hist_scan": "Potraži cikluse", "hist_import_n": "Uvezi cikluse: {n}", - "hist_goto_cycles": "Prikaži cikluse" + "hist_goto_cycles": "Prikaži cikluse", + "save_total_cycles": "Sačuvaj ukupan broj" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Mora biti iznad Praga isključivanja napajanja ({pot} W)", "threshold": "Mora biti ispod Praga zaustavljanja ({stop} W) da bi stupilo na snagu" }, + "price_entity": { + "power_sensor": "Ovo je Senzor snage uređaja, a ne cijena. Entitet cijene ima prednost nad statičnom cijenom ispod, pa bi ciklusi bili obračunati po vatima za kWh - očistite ovo polje.", + "energy_sensor": "Ovo je Brojilo energije uređaja, a ne cijena. Entitet cijene ima prednost nad statičnom cijenom ispod, pa bi ciklusi bili obračunati po očitanju brojila za kWh - očistite ovo polje.", + "device_class": "Klasa ovog senzora je {dc}, a ne cijena po kWh. Entitet cijene ima prednost nad statičnom cijenom ispod - očistite ovo polje ili ga usmjerite na senzor tarife.", + "unit": "Ovaj senzor očitava u {unit}, što nije cijena po kWh. Entitet cijene ima prednost nad statičnom cijenom ispod - očistite ovo polje ili ga usmjerite na senzor tarife." + }, "pump_stuck": { "duration": "Mora biti manje od Timeouta bez ažuriranja ({to} s) kako bi alarm zaglavljivanja aktivirao prije nego čuvar zaustavi ciklus", "timeout": "Mora premašiti Trajanje zaglavljivanja pumpe ({ps} s) kako bi alarm aktivirao prije prisilnog zaustavljanja ciklusa" @@ -349,7 +362,7 @@ "pg_settings_source": "Izvor postavki", "phase_catalog": "Katalog faza", "playground": "Testno okruženje", - "process_history": "Istorija procesa", + "process_history": "Obrada historije", "settings_history": "Historija postavki", "status": "Status", "status_graph": "Graf statusa", @@ -360,12 +373,12 @@ "wipe_history": "Obriši historiju", "recommendations": "Preporuke ({n})", "pg_detection_params": "Postavke detekcije", - "pg_timeline": "Vremenska linija događaja", "pg_outcome": "Ishod simulacije", "pg_across_cycles": "Kroz sve vaše cikluse", "community_store": "Zajednička trgovina", "online_account": "Zajednička trgovina i online funkcije", - "import_power_history": "Uvoz historije snage" + "import_power_history": "Uvoz historije snage", + "service_status": "Status servisa" }, "health": { "fair": "Prihvatljiva kvaliteta podudaranja", @@ -373,6 +386,10 @@ "poor": "⚠ Loša kvaliteta podudaranja" }, "lbl": { + "envelope_position_tip": "Položaj na vlastitoj snimljenoj krivulji podudarenog programa, {pct}%. Mjeri se poravnavanjem ovog ciklusa s profilom umjesto brojanjem vremena, pa ostaje tačan kada je pokretanje duže ili kraće nego obično. Osvježava se dok je uređaj tih, pa tokom aktivne faze može kasniti.", + "envelope_position": "krivulja {pct}%", + "aria_playground_chart3": "Interaktivni grafikon snage ciklusa: pređite mišem za prikaz vremena/snage, pomjerajte točkić ili koristite dva prsta za zumiranje, povucite za pomjeranje", + "device_unresolved": "Nedostupan uređaj ({id})", "drag_to_resize": "Prevucite za promjenu veličine", "actions": "Akcije", "activity": "Aktivnost", @@ -509,7 +526,7 @@ "profile_index": "indeks profila →", "profile_name": "Ime profila", "program": "Program", - "program_tip": "Zamijeni koji profil se poklapa s trenutnim ciklusom. Automatsko otkrivanje omogućava integraciji da automatski odabere najboljeg kandidata. Prikvačite određeni program za prisilno podudaranje kada automatsko otkrivanje nije ispravno ili znate što radi.", + "program_tip": "Zamijeni koji profil se poklapa s trenutnim ciklusom. Automatsko otkrivanje omogućava integraciji da automatski odabere najboljeg kandidata. Prikvačite određeni program za prisilno podudaranje kada automatsko otkrivanje nije ispravno ili znate što radi. Odaberite program prije pokretanja uređaja i primijenit će se čim počne sljedeći ciklus.", "progress": "Napredak", "raw_socket": "Sirova utičnica", "read_only": "Samo za čitanje", @@ -535,7 +552,7 @@ "settings_basic": "Osnovno", "settings_detail_level": "Nivo detalja postavki", "settings_overrides": "Zamjene postavki", - "show_debug": "Prikaži karticu za otklanjanje grešaka utakmice uživo na stranici Status (pouzdanje, dvosmislenost, najbolji kandidati)", + "show_debug": "Prikaži karticu za otklanjanje grešaka podudaranja uživo na stranici Status (pouzdanost, dvosmislenost, najbolji kandidati)", "show_expected": "Prikaži očekivani prekrivač krivulje (podudarni profil, narandžasta)", "show_raw": "Prikaži prekidač sirovih očitanja utičnice u živom grafu snage", "sparkline": "Trend trajanja posljednjih ciklusa", @@ -544,6 +561,7 @@ "stage4": "Faza 4 – slaganje", "status": "Status", "tail_trim": "Skraćivanje kraja (s)", + "tap_to_dismiss": "dodirnite grafikon da zatvorite", "timer_auto_pause": "Automatska pauza", "timer_min": "min", "timer_msg_placeholder": "Poruka (opcionalno, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Preostalo vrijeme (model)", "pg_time_left_model_tip": "Modelom procijenjeno preostalo vrijeme (procjenitelj faza + mješavina s ML), a ne statičko odbrojavanje", "phase": "Faza", - "pg_mode_simulate": "Simuliraj", "pg_mode_history": "Provjeri na historiji", - "pg_mode_sweep": "Ispitivanje", "pg_ev_detected": "Detektovano", "pg_ev_match": "Podudaranje potvrđeno", "pg_ev_match_changed": "Podudaranje promijenjeno", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parametar", "pg_objective": "Cilj", "pg_steps": "Koraci", - "pg_sweep_2d": "Toplotna karta 2D", - "pg_sweep_param_y": "2. parametar", "pg_obj_match": "Tačnost podudaranja", "pg_obj_endtiming": "Tačnost kraja", "pg_obj_falseend": "Stopa lažnih završetaka", "pg_obj_overrun": "Odstupanje trajanja", "pg_obj_ambiguity": "Stopa dvosmislenosti", - "pg_best_combo": "najbolja kombinacija", "pg_mode_optimize": "Optimiziraj", "pg_group_matching": "Podudaranje programa", "pg_zoom_hint": "dvostruki klik poništava zumiranje", @@ -706,7 +719,12 @@ "task_history_import_apply": "Uvoz ciklusa", "evidence_real_cycles": "Ciklusi koje je izvršila ova mašina", "evidence_reference_cycles": "Preuzeti iz zajedničke trgovine", - "evidence_backfill_cycles": "Pronađeni u uvezenoj historiji snage" + "evidence_backfill_cycles": "Pronađeni u uvezenoj historiji snage", + "zoom_hint_touch": "zumirajte sa dva prsta", + "zoom_hint_touch_zoomed": "zumirajte sa dva prsta · dvostruki dodir za resetiranje", + "cycles_since_service": "{since} / {total} ciklusa", + "total_cycles_run": "ciklusa ukupno", + "correct_total_cycles": "Ispravi ukupan broj" }, "log": { "all_levels": "Svi nivoi", @@ -756,9 +774,9 @@ "merge_cycles": "Spoji {n} ciklusa", "new_group": "Nova grupa profila", "new_phase": "Nova faza", - "process_history_msg": "Ponovo pokrenite uparivanje, osvježite prijedloge, trenirajte ML (ako je omogućeno) i preračunajte zdravlje ciklusa za sve pohranjene cikluse. Ovo može potrajati.", + "process_history_msg": "Ponovo pokrenite podudaranje, osvježite prijedloge, ponovo trenirajte ML (ako je omogućeno), preračunajte troškove prema zabilježenim cijenama energije i preračunajte zdravlje ciklusa za sve pohranjene cikluse. Ovo može potrajati.", "process_history_ok": "Obradi", - "process_history_title": "Obradi povijest", + "process_history_title": "Obradi historiju", "process_recording": "Snimanje procesa", "delete_maintenance_msg": "Izbrisati ovaj zapis o održavanju? Ovo se ne može poništiti.", "delete_maintenance_title": "Izbriši događaj održavanja", @@ -789,6 +807,7 @@ "history_import": "Uvoz historije snage" }, "msg": { + "pg_canvas_empty3": "Odaberite ciklus iznad i pritisnite Pokreni da ga simulirate. Zatim pređite mišem za prikaz vrijednosti, pomjerajte točkić ili koristite dva prsta za zumiranje i povucite za pomjeranje.", "tail_trim_hint": "Uklonite ovoliko sekundi sa kraja", "store_sibling_hint": "Nema ničega podijeljenog za vaš tačan model? Blisko srodan model iste marke obično je dobra početna tačka.", "store_declare_appliance": "Recite integraciji WashData koji uređaj posjedujete i ova kartica prikazat će konfiguracije koje su drugi podijelili za njega. Možete i upisati marku iznad kako biste pregledali katalog.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Prešao iznad uobičajenog pojasa snage za ~{n}s.", "auto_label_intro": "Dodijelite profile neoznačenim ciklusima čija pouzdanost podudaranja briše prag.", "automations_intro": "WashData pokreće događaje {start} / {end} i izlaže entitete, pa je obavijesti i radnje najbolje graditi kao normalne automatizacije Home Assistanta. Automatizacije koje koriste ovaj uređaj prikazane su ispod.", - "cleanup_intro": "Svaki označeni ciklus se preklapa. Označite nedostatke i izbrišite da biste očistili profil.", + "cleanup_intro": "Svi označeni ciklusi prikazani jedan preko drugog. Označite odstupajuće cikluse, a zatim im uklonite oznaku (ostaju u historiji, ali više ne formiraju ovaj profil) ili ih izbrišite.", "clear_debug_hint": "Uklonite pohranjene podatke za otklanjanje grešaka kako biste oslobodili prostor.", "collecting_data": "Prikupljanje podataka. Još potrebnih ciklusa prije početka finog podešavanja: {need} ({current}/{min}).", "compare_overlay_profiles": "Profili preklapanja (slabi)", @@ -902,7 +921,7 @@ "preferences_adv": "Postavke", "prefs_personal": "Ovo se odnosi samo na vaš račun Home Assistant.", "font_size_hint": "Povećajte ili smanjite sve u ovoj ploči. Primjenjuje se na vaš račun na ovom uređaju.", - "process_history_hint": "Ponovo pokrenite uparivanje na svim pohranjenim ciklusima, osvježite prijedloge podešavanja, ponovo trenirajte ML modele (ako su omogućeni) i preračunajte zdravlje ciklusa. Pokrenite nakon niza recenzija.", + "process_history_hint": "Ponovo pokrenite podudaranje na svim pohranjenim ciklusima, osvježite prijedloge podešavanja, ponovo trenirajte ML modele (ako su omogućeni), preračunajte troškove prema zabilježenim cijenama energije i preračunajte zdravlje ciklusa. Pokrenite ovo nakon niza pregleda.", "profile_deleted": "Profil izbrisan", "profile_poor_health_detail": "Ciklusi koji su dodijeljeni ovom profilu imaju nedosljedne oblike ili nisku pouzdanost. Razmislite o ponovnoj izgradnji omotača ili pregledu označenih ciklusa.", "profiles_intro": "Kliknite na profil za statistiku, faze i čišćenje. Grupirajte skoro identične programe (isti oblik/trajanje, različita temperatura ili centrifuga) tako da se pouzdano podudaranje bira između njih.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "Profil '{name}' ima nisku ocjenu podudaranja - njegovi noviji ciklusi znatno variraju ili se slabo podudaraju. Pregledajte njegove cikluse ili ponovo snimite profil kako bi prepoznavanje i procjene vremena ostali tačni.", "advisory_shape_drift": "Profil '{name}' je znatno odstupio od svog prvobitnog oblika potrošnje snage. Uređaj je možda vremenom promijenio ponašanje (npr. kamenac, habanje). Razmislite o ponovnom snimanju ovog profila sa novijim ciklusima.", "advisory_shape_drift_corr": "Profil '{name}' je znatno odstupio od svog prvobitnog oblika potrošnje snage (korelacija {corr}). Uređaj je možda vremenom promijenio ponašanje (npr. kamenac, habanje). Razmislite o ponovnom snimanju ovog profila sa novijim ciklusima.", + "advisory_unmatchable": "Profil '{name}' se nikada ne može podudariti: iza njega ne stoji nijedan ciklus sa podacima o snazi, pa WashData nema sa čime uporediti ciklus koji je u toku. Označite ciklus ovog programa (ili snimite novi) kako bi se ponovo uključio u podudaranje.", + "advisory_unmatchable_no_duration": "Profil '{name}' se nikada ne može podudariti: ima ciklus sa podacima o snazi, ali bez upotrebljivog trajanja, pa WashData nema referentnu dužinu prema kojoj bi se podudaranje mjerilo. Snimite ovaj program ponovo ili postavite njegovo očekivano trajanje kako bi se vratio u podudaranje.", "pg_no_alerts": "U ovom pokretanju nisu otkriveni problemi.", "pg_override_active": "Koriste se vaše izmijenjene postavke umjesto trenutnih", - "pg_history_intro": "Ponovo reproducirajte svoje nedavne cikluse kroz stvarni detektor i mehanizam podudaranja. Prvo izmijenite postavku detekcije u načinu Simuliraj da biste ovdje vidjeli poređenje prije/poslije.", "pg_history_empty": "Pritisnite Pokreni za reprodukciju svojih ciklusa.", "pg_history_summary": "{detected}/{total} detektovano · {correct} ispravno podudareno", "pg_sweep_no_metric": "Nedovoljno podataka za ocjenu ovog cilja.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Pretraživanje nije uspjelo.", "hist_scan_expired": "To pretraživanje više nije dostupno. Pokrenite ga ponovo.", "hist_import_failed": "Uvoz nije uspio.", - "imported_history_readonly": "Otkriveno u uvezenoj historiji snage. Utiče na podudaranje programa, ali se ne broji u vašu statistiku i ne može se skratiti ni podijeliti. Označite ga da mu dodijelite program." + "imported_history_readonly": "Otkriveno u uvezenoj historiji snage. Utiče na podudaranje programa, ali se ne broji u vašu statistiku i ne može se skratiti ni podijeliti. Označite ga da mu dodijelite program.", + "odometer_intro": "Ovaj ukupan broj samo raste. Brisanje zapisa o ciklusu ga ne mijenja i nastavlja se povećavati i nakon ograničenja sačuvane historije, tako da podsjetnici za servis ostaju tačni.", + "odometer_correct_hint": "Postavite ovo ako je WashData zabilježio ciklus koji se nikada nije dogodio ili ako je uređaj već obavio cikluse prije instaliranja WashData.", + "no_reminders_set": "Još nema postavljenih podsjetnika za servis.", + "toast_program_armed": "Program postavljen za sljedeći ciklus: {program}" }, "phase_desc": { "anti_crease": "Povremena kratka spuštanja nakon završetka za smanjenje bora.", @@ -1161,6 +1185,10 @@ "label": "Otpuštanje nakon tihe faze sušenja", "doc": "Kada ciklus pređe svoje očekivano trajanje, koliko dugo mašina mora ostati tiha (ispod Praga zaustavljanja) prije nego što WashData prestane čekati na završno ispumpavanje vode i završi ciklus. Povećajte ako vaš uređaj ima dugu tihu fazu sušenja prije kasnog završnog ispumpavanja koje se propušta - šire okno omogućava da naučeno trajanje prati sezonske promjene (hladnija dovodna voda = duži ciklusi) umjesto da ostane zaključano na staroj prosječnoj vrijednosti. Čekanje samo skraćuje u odnosu na interno ograničenje završnog ispumpavanja od 30 minuta, nikada ga ne produžava." }, + "anti_crease_finalize_ratio": { + "doc": "Koliko daleko u očekivanom trajanju podudarajućeg programa ciklus mora biti prije nego što WashData smije zaključiti da je tiho prevrtanje bubnja koje vidi završni rep anti-gužvanja i tu okončati ciklus. Očekivano trajanje je prosjek programa, pa kod sušilice čije vrijeme rada sa senzorskim sušenjem prati količinu rublja većina pokretanja nikada ne dostigne 98% tog prosjeka, pa rep ostaje stajati do rezervnog vremenskog ograničenja - često 15 do 40 minuta nakon što je mašina stvarno stala. Kod takve sušilice smanjite ovu vrijednost (npr. 0,75). Mašine za pranje rublja ostavite na zadanoj vrijednosti: mašina za pranje veći dio ciklusa provede ispod postavke Maks snaga anti-gužvanja, pa je ovaj udio jedino što razdvaja tihu fazu usred pranja od repa, a njegovo smanjenje može prepoloviti pranje. Ostavite prazno za zadanu vrijednost (0,98). Ovo je druga postavka od Omjera pametnog završetka, koji upravlja zasebnom provjerom ranog završetka.", + "label": "Omjer dovršetka anti-gužvanja" + }, "anti_wrinkle_enabled": { "doc": "Prepoznajte kratke impulse male snage koje sušilica emituje nakon glavne faze zagrijavanja i držite ih priključene na završeni ciklus umjesto da ih čitate kao nove cikluse.", "label": "Omogućiti detekciju anti-gužvanja" @@ -1182,7 +1210,7 @@ "label": "Maks snaga anti-gužvanja" }, "auto_label_confidence": { - "doc": "Ako je rezultat meča na kraju ciklusa na ili iznad ovog, program se automatski označava bez ikakvog upita za potvrdu. Podignite ga da zahtijeva veću sigurnost prije automatskog označavanja; snizite ga da više automatizujete. Radi u kombinaciji sa Learning Confidence ispod njega.", + "doc": "Ako je rezultat podudaranja na kraju ciklusa jednak ili veći od ovoga, program se automatski označava bez ikakvog upita za potvrdu. Podignite ga da zahtijeva veću sigurnost prije automatskog označavanja; snizite ga da više automatizujete. Radi u kombinaciji s Pouzdanošću učenja ispod njega.", "label": "Pouzdanost automatskog označavanja" }, "auto_maintenance": { @@ -1193,6 +1221,10 @@ "doc": "Ciklusi kraći od ovoga se odbacuju kao ciklusi duhova (probni radovi, otvaranje vrata da se doda čarapa).", "label": "Min trajanje ciklusa" }, + "curve_preroll_seconds": { + "doc": "Koliko sekundi očitanja iz prekinutih pokušaja pokretanja WashData smije prenijeti na početak krivulje ciklusa. Krivulja ciklusa obično počinje pokušajem pokretanja koji konačno uspije, pa kod mašine koja nekoliko puta pokuša krenuti prije nego što se ustali - izbor programa, zaključavanje vrata, prvo punjenje vodom - prve minute stvarne aktivnosti nedostaju na svakoj krivulji. Postavite ovu vrijednost malo iznad najdužeg takvog zaleta koji vaša mašina ima (300 s je za većinu više nego dovoljno). 0 isključuje ovu funkciju. Imajte na umu da uključivanje pomjera zabilježeni početak ranije, pa ciklusi zabilježeni prije i poslije promjene nose različita trajanja za isti program; naučeni prosjeci će odstupati dok stariji ciklusi ne isteknu.", + "label": "Uvodne sekunde krivulje" + }, "delay_confirm_seconds": { "doc": "Napajanje mora ostati u opsegu pripravnosti ovoliko dugo prije nego što se uređaj tretira kao da čeka da se pokrene, a ne da radi.", "label": "Sekunde potvrde" @@ -1230,7 +1262,7 @@ "label": "Broj ponavljanja kraja" }, "energy_price_entity": { - "doc": "Senzor sa trenutnom cijenom električne energije po kWh (npr. dinamička tarifa). Ima prednost nad statičnom cijenom ispod. Svaki ciklus zamrzava cijenu koja je na snazi kada se završi.", + "doc": "Senzor s trenutnom cijenom struje po kWh (npr. dinamična tarifa). Mora biti cijena, a ne vlastiti entitet snage ili energije utičnice: ima prednost nad statičnom cijenom ispod, pa bi brojilo kWh ovdje obračunalo svaki ciklus po očitanju brojila umjesto po vašoj tarifi. Kada je Vremenski ponderisan trošak uključen, svaki ciklus se naplaćuje po cijeni koja je važila u svakom trenutku njegovog rada; u suprotnom se na njega zamrzava cijena koja je važila kada je završio.", "label": "Entitet cijene energije" }, "energy_price_static": { @@ -1258,7 +1290,7 @@ "label": "Invertirati vanjski okidač (okidač pri ISKLJUČIVANJU)" }, "learning_confidence": { - "doc": "Ako rezultat podudaranja pada između ovog i Auto-Label Confidence, povratno obavještenje traži od vas da potvrdite identificirani program. Ispod ovog rezultata meč je previše neizvjestan da bi se pojavio. Mora se držati ispod pouzdanosti automatske oznake.", + "doc": "Ako rezultat podudaranja padne između ovog i Pouzdanosti automatskog označavanja, obavještenje s povratnim informacijama traži od vas da potvrdite identificirani program. Ispod ovog rezultata podudaranje je previše nesigurno da bi se prikazalo. Mora se držati ispod Pouzdanosti automatskog označavanja.", "label": "Pouzdanost učenja" }, "linked_device": { @@ -1326,7 +1358,7 @@ "label": "Pokrenuti HA događaje za obavijesti" }, "notify_icon": { - "doc": "Opcionalna mdi ikona za obavještenje (npr. mdi:mašina za pranje rublja). Ostavite prazno za zadanu platformu.", + "doc": "Mala ikona prikazana na obavještenju (npr. mdi:washing-machine). Android je prikazuje u statusnoj traci; iOS je prikazuje umjesto ikone aplikacije i zahtijeva mobilnu aplikaciju Home Assistant (companion app) 2026.8 ili noviju. Ostavite prazno za zadanu ikonu platforme.", "label": "Ikona obavijesti" }, "notify_live_chronometer": { @@ -1426,7 +1458,7 @@ "label": "Prag podudaranja" }, "profile_unmatch_threshold": { - "doc": "Ako utakmica u sredini ciklusa uživo padne ispod ovog rezultata, privremena identifikacija se briše. Držite ga malo ispod praga podudaranja tako da kratko smanjenje sličnosti ne bi vratilo ekran na neusporedivo.", + "doc": "Ako podudaranje uživo usred ciklusa padne ispod ovog rezultata, privremena identifikacija se briše. Držite ga malo ispod Praga podudaranja kako kratak pad sličnosti ne bi vratio prikaz na stanje bez podudaranja.", "label": "Prag nepodudaranja" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Sekunde kada pumpa može raditi neprekidno prije nego što bude označena kao da je moguće zaglavljena (pokreće događaj zaglavljena pumpa).", "label": "Trajanje zaglavljivanja pumpe" }, - "running_dead_zone": { - "doc": "Nakon što ciklus započne, padovi snage u ovom prozoru se zanemaruju. Mašine za pranje rublja se pune hladnom vodom (pada blizu 0 W prije zagrijavanja) - bez ove zaštite ta faza punjenja izgleda kao završetak ciklusa. Ovo NE preskače podatke: puni trag snage se snima od T=0. Mehanizam za prijedloge mjeri stvarni obrazac pokretanja vašeg stroja i automatski ga određuje.", - "label": "Mrtva zona pri radu" - }, "sampling_interval": { "doc": "Očekivano vrijeme između očitavanja senzora - koristi se za veličinu prozora za izglađivanje i pravilno pokretanje odbijanja. Svako ažuriranje senzora se snima bez obzira na ovu vrijednost; kalibrira samo nizvodne proračune. Mehanizam za prijedloge mjeri stvarnu kadencu vašeg senzora iz prošlih ciklusa i automatski ga postavlja.", "label": "Interval uzorkovanja" @@ -1568,13 +1596,17 @@ "label": "Interval profila snage", "doc": "Veličina segmenta za atribut senzora power_profile po profilu (polje prosječnih vata po slotu koje koriste vanjski planeri poput EMHASS i tibber_prices). Manji segmenti zadržavaju kratke skokove snage oštrima; veći segmenti zaglađuju oblik. Zadano 15 min. Samo za čitanje; ne utječe na otkrivanje." }, + "notify_live_silent": { + "label": "Tiha ažuriranja uživo", + "doc": "Samo iOS. Napredak uživo osvježavajte tiho: svako ažuriranje stiže bez zvuka i vibracije, s nižim prioritetom koji štedi bateriju i koji iOS može nakratko grupisati. Prvo ažuriranje u ciklusu (ono koje pokreće Live Activity), kao i obavještenja o početku i završetku, i dalje se oglašavaju kao i obično. Isključite ovo da biste bili obaviješteni pri svakom ažuriranju." + }, "notify_live_sticky": { "label": "Zadrži živo obavještenje pri dodiru", "doc": "Samo Android. Živo obavještenje o napretku postavite kao trajno (prikvačeno) kako dodir ne bi odbacio aktivnu nit. Isključeno zadržava zadano ponašanje gdje dodir odbacuje obavještenje." }, "notify_live_click_action": { - "label": "Odredište dodira na živom obavještenju", - "doc": "Samo Android. Gdje dodir na živo obavještenje o napretku otvara (npr. /lovelace/laundry ili cijeli URL) umjesto početne stranice aplikacije. Ostavite prazno za zadano." + "label": "Odredište dodira na obavještenju", + "doc": "Kuda vodi dodir obavještenja sa ovog uređaja, na sistemima Android i iOS. Prazno polje otvara WashData panel na ovom uređaju. Unesite putanju kontrolne table (npr. /lovelace/laundry) ili cijeli URL da vodi negdje drugdje, ili „none“ ako dodir ne treba ništa otvarati." }, "door_opens_at_end": { "label": "Vrata se automatski otvaraju na kraju", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Ciklusi koji formiraju program", "doc": "Koji se ciklusi koriste za izgradnju krivulje snage svakog programa i za podudaranje završenog ciklusa s njom. Kada odznačite neku vrstu, ona više ne utiče na vaše programe, ali se ništa ne briše - ciklusi ostaju na listi Ciklusi i još uvijek se mogu označiti ili izbrisati. Korisno ako ne vjerujete uvezenim podacima. Na statistiku to ne utiče: uvijek se računaju samo ciklusi koje je ova mašina zaista izvršila. Odznačavanje svega se ignoriše jer se program bez ikakvih ciklusa nikada ne bi mogao podudariti." + }, + "energy_price_dynamic": { + "label": "Vremenski ponderisan trošak", + "doc": "Naplaćuje svaki ciklus po cijeni koja je važila u svakom trenutku njegovog rada, umjesto po jednoj cijeni koja je važila kada je završio. Odnosi se samo na entitet cijene (statična cijena se ne može mijenjati). Ne treba dodatno podešavanje: cijena se prati uživo dok ciklus traje, a Obrada historije u Dijagnostici ponovo obračunava starije cikluse iz recordera. Isključeno = klasično ponašanje, jedna cijena zamrznuta na kraju." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Dijeljenje…", "deleting": "Brisanje…", "imported": "Uvezeno", - "preparing": "Priprema…" + "preparing": "Priprema…", + "unlabelling": "Uklanjanje oznake…" }, "suggestion": { "both_agree": "WashData preporučuje", @@ -1745,10 +1782,6 @@ "higher": "Potvrđuje samo visoko pouzdana podudaranja; više ciklusa ostaje neoznačeno", "lower": "Podudara više ciklusa; neki mogu biti blago pogrešni" }, - "running_dead_zone": { - "higher": "Zanemaruje kratke skokove snage tokom mirovanja – manje osjetljiv na šum", - "lower": "Reagira na kraće impulse snage – otkriva brzu prolaznu aktivnost" - }, "start_threshold_w": { "higher": "Sprečava lažne startove od kratkih skokova snage", "lower": "Otkriva programe s niskom snagom; može reagirati na šum" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} čistih ciklusa ({pct}%) imalo je unutrašnju pauzu dužu od 60s koja se nastavila; zahtijevaj {val} potvrdu(e) kraja.", "learning_confidence": "Pouzdanost p05 za {cycles} korisnički označenih ciklusa ({p05}); ispod ove vrijednosti zahtijevaj provjeru.", "match_interval": "Na osnovu uočene učestalosti ažuriranja (medijana={median}s) * 10.", + "match_interval_capped": "Ograničeno tako da {persistence} uzastopnih podudaranja stane u {pct}% najkraćeg programa ({shortest}s); sama učestalost ažuriranja (medijana={median}s) predložila bi duži interval.", + "match_interval_floored": "Već bi i sam najkraći program ({shortest}s) ovu vrijednost ograničio na {cap}s, ispod minimuma od {minimum}s, pa se zadržava na minimumu: niz od {persistence} uzastopnih podudaranja traje {budget}s, što je više od {pct}% tog programa. Podudaranje se pokreće samo kada stigne novo očitanje (medijana={median}s), pa minimum ne donosi dodatno opterećenje.", "max_duration_ratio": "Na osnovu trajanja označenih ciklusa (p95={p95}).", "min_active": "Na osnovu najmanje radne snage ({min}W) uočene u posljednjem ciklusu.", "min_duration_ratio": "Zadržano što nižim kako bi se program prepoznao rano u ciklusu; pragovi pouzdanosti i dvosmislenosti sprečavaju prijevremene potvrde.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Snimanje je zaustavljeno", "review_saved": "Recenzija je sačuvana", "settings_conflicts": "Ispravite istaknute sukobe postavki prije spremanja.", + "settings_no_changes": "Nema promjena za spremanje", "settings_reverted": "Postavke vraćene; ponovno učitavanje integracije", "settings_saved": "Postavke sačuvane; ponovno učitavanje integracije", "suggestions_applied": "Primijenjeni prijedlozi; ponovno učitavanje integracije", @@ -1949,7 +1985,11 @@ "export_selective_done": "Izvoz preuzet", "import_selective_done": "Uvezeno {profiles} profila i {cycles} ciklusa", "hist_csv_required": "Prvo učitajte CSV fajl ili zalijepite njegov sadržaj", - "file_read_failed": "Nije bilo moguće pročitati taj fajl" + "file_read_failed": "Nije bilo moguće pročitati taj fajl", + "unlabel_done": "Uklonjena oznaka s ciklusa: {count}", + "unlabel_failed": "Uklanjanje oznake nije uspjelo: {error}", + "odometer_saved": "Ukupan broj ciklusa je sačuvan", + "odometer_save_failed": "Nije moguće sačuvati ukupan broj: {error}" }, "trend": { "down": "Opadajući trend", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Mašina za pranje posuđa: sekunde tišine nakon očekivanog trajanja prije nego što se otpusti čekanje na završno ispumpavanje vode", "smart_termination_duration_ratio": "Udio očekivanog trajanja podudarajućeg programa koji ciklus mora dostići prije nego što ga Pametni završetak može ranije završiti; smanjite ga za mašine ovisne o količini rublja ili temperaturi", + "anti_crease_finalize_ratio": "Udio očekivanog trajanja podudarajućeg programa koji ciklus mora dostići prije nego što se završni rep prevrtanja anti-gužvanja smije dovršiti; smanjite ga kod sušilica s dugim repom prevrtanja, mašine za pranje rublja ostavite na zadanoj vrijednosti", "anti_wrinkle_enabled": "Upij impulse bubnja nakon glavne faze, a ne čitaj ih kao nove cikluse", "anti_wrinkle_exit_power": "Snaga mora pasti ispod ove vrijednosti između impulsa da bi anti-gužvanje ostalo aktivno", "anti_wrinkle_idle_timeout": "Dozvoljeno vrijeme mirovanja između dva impulsa bubnja prije nego što se anti-gužvanje završi", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Faza 3: udaljenost polu-zasićenja DDTW; manja = osjetljivija na oblik (zadano 30)", "dtw_refine_top_n": "Faza 3: kandidati koje DTW ponovo ocjenjuje; povećajte na 7-9 ako ispravni profil zauzme 4.-5. mjesto (zadano 5)", "duration_scale": "Faza 4: omjer logaritma gdje se slaganje trajanja prepolovi; manje = stroža kazna (zadano 0,175)", - "energy_scale": "Faza 4: omjer logaritma gdje se slaganje energije prepolovi; manje = stroža kazna (zadano 0,25)" + "energy_scale": "Faza 4: omjer logaritma gdje se slaganje energije prepolovi; manje = stroža kazna (zadano 0,25)", + "curve_preroll_seconds": "Sekunde očitanja iz prekinutih pokušaja pokretanja prenesene na početak krivulje ciklusa; 0 = isključeno" }, "col": { "profile_tip": "Naziv podudarnog programa. Bez oznake znači da se na kraju ciklusa nijedan profil nije podudarao.", @@ -1998,10 +2040,11 @@ "date_tip": "Datum i vrijeme početka ciklusa.", "duration_tip": "Ukupno vrijeme rada ciklusa od početka do kraja.", "energy_tip": "Ukupna potrošena energija (kWh). Izračunato integracijom snage kroz vrijeme.", - "cost_tip": "Trošak energije za ovaj ciklus, zamrznut po završetku prema tada važećoj cijeni (energija x cijena po kWh). Postavite cijenu u Postavkama da biste ga popunili.", + "cost_tip": "Trošak energije za ovaj ciklus, zamrznut po završetku. Kada je Vremenski ponderisan trošak uključen, kriva snage se naplaćuje po cijeni koja važi u svakom trenutku; u suprotnom energija x jedna cijena koja važi na kraju. Postavite cijenu u Postavkama da se popuni.", "confidence_tip": "Pouzdanost podudaranja profila (0-100%). Koliko se tačno kriva snage ciklusa podudarala s identificiranim programom.", "health_tip": "Zdravlje ciklusa ML (više = bolje). Kliknite ciklus da biste ga pregledali i ocijenili.", - "flags_tip": "Oznake zajednice i pokazatelji kvalitete podataka" + "flags_tip": "Oznake zajednice i pokazatelji kvalitete podataka", + "cost_dynamic_tip": "Vremenski ponderisano: naplaćeno po cijeni koja je važila tokom ciklusa (efektivno {price} {cur}/kWh)." }, "stat": { "avg": "pros.", @@ -2028,9 +2071,9 @@ "finished": "Ciklus je dostigao završno stanje i završio." }, "store": { + "search_ph2": "Pretraži po marki ili modelu…", "your_model_tip": "Ovo je uređaj koji ste naveli u Postavkama", "your_model": "Vaš", - "search_brand_ph": "Pretraži po marki…", "programs_count": "Programi: {n}", "browse": "Pregledaj", "device": "Uređaj", @@ -2076,7 +2119,8 @@ "golden": "Ponovna obrada: popunjavanje referentnih", "suggestions": "Ponovna obrada: prijedlozi", "ml_training": "Ponovna obrada: ML obuka", - "health": "Ponovna obrada: zdravlje ciklusa" + "health": "Ponovna obrada: zdravlje ciklusa", + "costs": "Ponovna obrada: troškovi energije" }, "pg_sweep": { "optimize": "Optimizacija: {param}" diff --git a/custom_components/ha_washdata/translations/panel/cs.json b/custom_components/ha_washdata/translations/panel/cs.json index 10691d73..ad496359 100644 --- a/custom_components/ha_washdata/translations/panel/cs.json +++ b/custom_components/ha_washdata/translations/panel/cs.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Ke konci tento program zhruba na {mins} min ztichne a před dokončením odebírá asi {watts} W po dobu {secs} s. Zaznamenáno u {seen} z {measured} změřených cyklů: spotřebič to nedělá při každém běhu.", + "quiet_tail": "tichý konec ~{mins} min", "artifact_tip": "Zjištěno {n} anomálií (např. dveře otevřené uprostřed cyklu) – otevřete, abyste je viděli na grafu", "auto": "(automaticky zjištěno)", "built_in_tag": "vestavěný", @@ -77,11 +79,14 @@ "approved": "Schváleno", "awaiting": "Čeká na schválení", "imported_tip": "Importováno z komunitního obchodu. Používá se pouze pro porovnávání, nezapočítává se do statistik.", + "unmatchable": "nelze rozpoznat", "not_importable": "nedostupné", "exists": "existuje", - "backfilled_tip": "Detekováno v importované historii výkonu. Ovlivňuje pouze přiřazování programů, nezapočítává se do statistik." + "backfilled_tip": "Detekováno v importované historii výkonu. Ovlivňuje pouze přiřazování programů, nezapočítává se do statistik.", + "armed": "(použije se na další cyklus)" }, "btn": { + "reset_zoom": "Obnovit přiblížení", "set_brand_model": "Nastavit značku a model", "refresh_catalog": "Obnovit katalog", "add_device": "+ Přidat zařízení", @@ -217,6 +222,8 @@ "trim": "Oříznout", "trim_split": "Oříznout / Rozdělit", "undo": "Zpět", + "unlabel_selected": "Odznačit vybrané ({n})", + "unlabel_selected_tip": "Odebere u vybraných cyklů štítek profilu. Zůstanou ve vaší historii a lze je později znovu označit - jen přestanou tvořit tento profil.", "use": "Použít", "wipe_all": "Vymazat všechna data", "delete_selected": "Smazat vybrané ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Použít nejlepší", "pg_apply_to_settings": "Uložit do nastavení", "pg_apply_to_settings_tip": "Zkopírovat hodnoty, které jste zde upravili, do aktivního nastavení tohoto zařízení", - "search": "Hledat", "import": "Importovat", "disconnect": "Odpojit", "connect_github": "Připojit GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Načíst z Home Assistant", "hist_scan": "Vyhledat cykly", "hist_import_n": "Importovat cykly: {n}", - "hist_goto_cycles": "Zobrazit cykly" + "hist_goto_cycles": "Zobrazit cykly", + "save_total_cycles": "Uložit celkový počet" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Musí být nad Prahem vypnutí napájení ({pot} W)", "threshold": "Musí být pod Prahem zastavení ({stop} W), aby měl účinek" }, + "price_entity": { + "power_sensor": "Toto je Snímač výkonu zařízení, ne cena. Entita ceny má přednost před statickou cenou níže, takže by se cykly účtovaly ve wattech za kWh - vymažte toto pole.", + "energy_sensor": "Toto je Měřič energie zařízení, ne cena. Entita ceny má přednost před statickou cenou níže, takže by se cykly účtovaly podle stavu měřiče za kWh - vymažte toto pole.", + "device_class": "Třída tohoto senzoru je {dc}, ne cena za kWh. Entita ceny má přednost před statickou cenou níže - vymažte toto pole nebo jej nasměrujte na senzor tarifu.", + "unit": "Tento senzor měří v {unit}, což není cena za kWh. Entita ceny má přednost před statickou cenou níže - vymažte toto pole nebo jej nasměrujte na senzor tarifu." + }, "pump_stuck": { "duration": "Musí být menší než Timeout bez aktualizace ({to} s), aby alarm zaseknutí vyskočil dříve než hlídač ukončí cyklus", "timeout": "Musí překročit Délku zaseknutého čerpadla ({ps} s), aby alarm vyskočil dříve než je cyklus vynuceně zastaven" @@ -349,7 +362,7 @@ "pg_settings_source": "Zdroj nastavení", "phase_catalog": "Katalog fází", "playground": "Testovací prostředí", - "process_history": "Historie procesu", + "process_history": "Zpracování historie", "settings_history": "Historie nastavení", "status": "Stav", "status_graph": "Stavový graf", @@ -360,12 +373,12 @@ "wipe_history": "Vymazat historii", "recommendations": "Doporučení ({n})", "pg_detection_params": "Nastavení detekce", - "pg_timeline": "Časová osa událostí", "pg_outcome": "Výsledek simulace", "pg_across_cycles": "Napříč vašimi cykly", "community_store": "Komunitní obchod", "online_account": "Komunitní obchod a online funkce", - "import_power_history": "Import historie výkonu" + "import_power_history": "Import historie výkonu", + "service_status": "Stav servisu" }, "health": { "fair": "Přijatelná shoda", @@ -373,6 +386,10 @@ "poor": "⚠ Špatná shoda" }, "lbl": { + "envelope_position_tip": "Pozice na vlastní zaznamenané křivce shodného programu, {pct}%. Určuje se porovnáním tvaru tohoto cyklu s profilem, ne odpočtem času, takže zůstává správná i při delším nebo kratším běhu. Obnovuje se, když je spotřebič v klidu, takže během aktivní fáze může zaostávat.", + "envelope_position": "křivka {pct}%", + "aria_playground_chart3": "Interaktivní graf výkonu cyklu: najetím zobrazíte čas/výkon, kolečkem nebo dvěma prsty přiblížíte, tažením posunete", + "device_unresolved": "Nedostupné zařízení ({id})", "drag_to_resize": "Přetažením změníte velikost", "actions": "Akce", "activity": "Aktivita", @@ -509,7 +526,7 @@ "profile_index": "index profilu →", "profile_name": "Název profilu", "program": "Program", - "program_tip": "Přepište, který profil se páruje s aktuálním cyklem. Automatická detekce umožňuje integraci automaticky vybrat nejlepší shodu. Připněte konkrétní program, aby se vynuceně pároval, pokud automatická detekce chybuje nebo víte, co běží.", + "program_tip": "Přepište, který profil se páruje s aktuálním cyklem. Automatická detekce umožňuje integraci automaticky vybrat nejlepší shodu. Připněte konkrétní program, aby se vynuceně pároval, pokud automatická detekce chybuje nebo víte, co běží. Vyberte program před spuštěním spotřebiče a použije se, jakmile začne další cyklus.", "progress": "Pokrok", "raw_socket": "Surový signál", "read_only": "Pouze pro čtení", @@ -544,6 +561,7 @@ "stage4": "Fáze 4 – shoda", "status": "Stav", "tail_trim": "Oříznutí konce (s)", + "tap_to_dismiss": "klepnutím na graf zavřete", "timer_auto_pause": "Automatická pauza", "timer_min": "min", "timer_msg_placeholder": "Zpráva (volitelné, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Zbývající čas (model)", "pg_time_left_model_tip": "Modelem odhadovaný zbývající čas (odhad fáze + kombinace s ML), nikoli statické odpočítávání", "phase": "Fáze", - "pg_mode_simulate": "Simulovat", "pg_mode_history": "Otestovat na historii", - "pg_mode_sweep": "Prohledávání", "pg_ev_detected": "Detekováno", "pg_ev_match": "Shoda potvrzena", "pg_ev_match_changed": "Shoda změněna", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parametr", "pg_objective": "Cíl", "pg_steps": "Kroky", - "pg_sweep_2d": "Teplotní mapa 2D", - "pg_sweep_param_y": "2. parametr", "pg_obj_match": "Přesnost shody", "pg_obj_endtiming": "Přesnost konce", "pg_obj_falseend": "Míra falešných konců", "pg_obj_overrun": "Odchylka trvání", "pg_obj_ambiguity": "Míra nejednoznačnosti", - "pg_best_combo": "nejlepší kombinace", "pg_mode_optimize": "Optimalizovat", "pg_group_matching": "Přiřazování programů", "pg_zoom_hint": "dvojklikem obnovíte přiblížení", @@ -706,7 +719,12 @@ "task_history_import_apply": "Import cyklů", "evidence_real_cycles": "Cykly, které tento spotřebič provedl", "evidence_reference_cycles": "Stažené z komunitního obchodu", - "evidence_backfill_cycles": "Nalezené v importované historii výkonu" + "evidence_backfill_cycles": "Nalezené v importované historii výkonu", + "zoom_hint_touch": "dvěma prsty přiblížíte", + "zoom_hint_touch_zoomed": "dvěma prsty přiblížíte · dvojitým klepnutím obnovíte", + "cycles_since_service": "{since} / {total} cyklů", + "total_cycles_run": "cyklů celkem", + "correct_total_cycles": "Opravit celkový počet" }, "log": { "all_levels": "Všechny úrovně", @@ -756,7 +774,7 @@ "merge_cycles": "Sloučit {n} cyklů", "new_group": "Nová skupina profilů", "new_phase": "Nová fáze", - "process_history_msg": "Znovu spustí párování, obnoví návrhy, přetrénuje ML (je-li povoleno) a přepočítá zdraví cyklů pro všechny uložené cykly. Může to chvíli trvat.", + "process_history_msg": "Znovu spustí vyhodnocení shody, obnoví návrhy, přetrénuje ML (je-li povoleno), přepočítá náklady podle zaznamenaných cen energie a přepočítá zdraví cyklů u všech uložených cyklů. Může to chvíli trvat.", "process_history_ok": "Zpracovat", "process_history_title": "Zpracovat historii", "process_recording": "Procesní záznam", @@ -789,6 +807,7 @@ "history_import": "Import historie výkonu" }, "msg": { + "pg_canvas_empty3": "Vyberte cyklus výše a stisknutím Spustit jej nasimulujte. Poté najetím zobrazíte hodnoty, kolečkem nebo dvěma prsty přiblížíte a tažením posunete.", "tail_trim_hint": "Kolik sekund odstranit z konce", "store_sibling_hint": "Pro váš konkrétní model není nic nasdíleno? Velmi podobný model od stejné značky je obvykle dobrý výchozí bod.", "store_declare_appliance": "Řekněte WashData, jaký spotřebič máte, a tato karta zobrazí nastavení, která pro něj nasdíleli ostatní uživatelé. Můžete také zadat značku výše a jen se rozhlédnout.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Překročil obvyklé pásmo výkonu po dobu ~{n} s.", "auto_label_intro": "Přiřaďte profily neoznačeným cyklům, jejichž spolehlivost shody překročí prahovou hodnotu.", "automations_intro": "WashData vyvolává události {start} / {end} a zpřístupňuje entity. Oznámení a akce se nejlépe nastavují jako standardní automatizace Home Assistant. Automatizace používající toto zařízení se zobrazují níže.", - "cleanup_intro": "Každý označený cyklus se překrývá. Zaškrtnutím odlehlých hodnot a odstraněním se profil vyčistí.", + "cleanup_intro": "Všechny označené cykly překryté přes sebe. Zaškrtněte odlehlé cykly a pak je odznačte (zůstanou v historii, ale už netvoří tento profil), nebo je rovnou smažte.", "clear_debug_hint": "Odstraňte uložená ladicí data, abyste uvolnili místo.", "collecting_data": "Shromažďování dat. Zbývající cykly do zahájení doladění: {need} ({current}/{min}).", "compare_overlay_profiles": "Překryvné profily (slabé)", @@ -902,7 +921,7 @@ "preferences_adv": "Předvolby", "prefs_personal": "Platí pouze pro váš účet Home Assistant.", "font_size_hint": "Zvětšete nebo zmenšete vše v tomto panelu. Platí pro váš účet na tomto zařízení.", - "process_history_hint": "Znovu spustí párování na všech uložených cyklech, obnoví návrhy ladění, přetrénuje modely ML (je-li povoleno) a přepočítá zdraví cyklů. Spusťte po skupině revizí.", + "process_history_hint": "Znovu spustí vyhodnocení shody u všech uložených cyklů, obnoví návrhy ladění, přetrénuje modely ML (je-li povoleno), přepočítá náklady cyklů podle zaznamenaných cen energie a přepočítá zdraví cyklů. Spusťte po sérii kontrol.", "profile_deleted": "Profil smazán", "profile_poor_health_detail": "Cykly přiřazené k tomuto profilu mají nekonzistentní tvary nebo nízkou spolehlivost. Zvažte přestavbu obálky nebo revizi označených cyklů.", "profiles_intro": "Kliknutím na profil zobrazíte statistiky, fáze a vyčištění. Seskupte téměř identické programy (stejný tvar/doba trvání, různá teplota nebo odstřeďování), takže se mezi nimi dá spolehlivě vybrat.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "'{name}' má nízké skóre shody - jeho poslední cykly se hodně liší nebo se špatně rozpoznávají. Projděte jeho cykly nebo profil nahrajte znovu, aby rozpoznávání a odhady času zůstaly přesné.", "advisory_shape_drift": "'{name}' se výrazně odchýlil od původního tvaru křivky výkonu. Chování spotřebiče se mohlo časem změnit (např. vodní kámen, opotřebení). Zvažte opětovné nahrání tohoto profilu z posledních cyklů.", "advisory_shape_drift_corr": "'{name}' se výrazně odchýlil od původního tvaru křivky výkonu (korelace {corr}). Chování spotřebiče se mohlo časem změnit (např. vodní kámen, opotřebení). Zvažte opětovné nahrání tohoto profilu z posledních cyklů.", + "advisory_unmatchable": "'{name}' nelze nikdy rozpoznat: nestojí za ním žádný cyklus s daty o výkonu, takže WashData nemá s čím probíhající cyklus porovnat. Označte tímto programem některý cyklus (nebo jeden zaznamenejte), aby se profil vrátil do rozpoznávání.", + "advisory_unmatchable_no_duration": "'{name}' nelze nikdy rozpoznat: má cyklus s daty o výkonu, ale bez použitelné doby trvání, takže WashData nemá délku, podle které by šlo shodu poměřit. Zaznamenejte tento program znovu nebo nastavte jeho očekávanou dobu trvání, aby se vrátil do rozpoznávání.", "pg_no_alerts": "V tomto běhu nebyly zjištěny žádné problémy.", "pg_override_active": "Používají se vaše upravená nastavení oproti aktuálním", - "pg_history_intro": "Přehrajte své nedávné cykly přes skutečný detektor a párování. Nejprve upravte nastavení detekce v režimu Simulovat, abyste zde viděli porovnání před/po.", "pg_history_empty": "Stiskněte Spustit pro přehrání vašich cyklů.", "pg_history_summary": "{detected}/{total} detekováno · {correct} správně spárováno", "pg_sweep_no_metric": "Nedostatek dat pro vyhodnocení tohoto cíle.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Vyhledávání se nezdařilo.", "hist_scan_expired": "Toto vyhledávání už není dostupné. Spusťte je prosím znovu.", "hist_import_failed": "Import se nezdařil.", - "imported_history_readonly": "Detekováno v importované historii výkonu. Ovlivňuje přiřazování programů, ale nezapočítává se do vašich statistik a nelze jej oříznout ani rozdělit. Označením mu přiřaďte program." + "imported_history_readonly": "Detekováno v importované historii výkonu. Ovlivňuje přiřazování programů, ale nezapočítává se do vašich statistik a nelze jej oříznout ani rozdělit. Označením mu přiřaďte program.", + "odometer_intro": "Tento celkový počet pouze roste. Smazání záznamu o cyklu jej nezmění a počítá dál i za limitem uložené historie, takže připomínky servisu zůstávají správné.", + "odometer_correct_hint": "Nastavte, pokud WashData zaznamenala cyklus, který nikdy neproběhl, nebo pokud spotřebič už absolvoval cykly před instalací WashData.", + "no_reminders_set": "Zatím nejsou nastaveny žádné připomínky servisu.", + "toast_program_armed": "Program nastaven pro další cyklus: {program}" }, "phase_desc": { "anti_crease": "Občasné krátké otáčení bubnu po dokončení cyklu pro prevenci mačkání.", @@ -1090,7 +1114,7 @@ "group_name": "např. Bavlna 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Popisek\"}]", "notify": "přidat službu pro oznámení…", - "notify_services_list": "notify.mobile_app_telefon, ...", + "notify_services_list": "notify.mobile_app_phone, ...", "person": "přidat osobu…", "phase_name": "např. Předpírka", "profile_name": "např. Bavlna 40 °C", @@ -1161,6 +1185,10 @@ "label": "Uvolnění po tiché fázi sušení", "doc": "Jakmile cyklus překročí svou očekávanou dobu trvání, jak dlouho musí myčka zůstat v klidu (pod Prahem zastavení), než WashData přestane čekat na závěrečné odčerpání vody a ukončí cyklus. Zvyšte hodnotu, pokud má váš spotřebič dlouhou tichou fázi sušení před pozdním závěrečným odčerpáním, které uniká detekci - širší okno umožňuje, aby naučená doba trvání sledovala sezonní posun (studenější přiváděná voda = delší cykly) místo toho, aby zůstala uzamčená na starém průměru. Čekání pouze zkracuje vzhledem k internímu 30minutovému limitu čekání na závěrečné odčerpání, nikdy jej neprodlužuje." }, + "anti_crease_finalize_ratio": { + "doc": "Jak daleko v očekávané době trvání přiřazeného programu musí cyklus být, než smí WashData vyhodnotit pozorované otáčení bubnu s nízkým příkonem jako ocas ochrany před zmačkáním a cyklus tam ukončit. Očekávaná doba trvání je průměr programu, takže u sušičky, jejíž doba senzorového sušení se řídí náplní, většina běhů nikdy nedosáhne 98 % této hodnoty a ocas zůstane viset až do záložního časového limitu - často 15 až 40 minut poté, co se stroj skutečně zastavil. U takové sušičky hodnotu snižte (např. 0,75). U praček ponechte výchozí hodnotu: pračka stráví většinu cyklu pod hodnotou Max. výkon ochrany před zmačkáním, takže tento zlomek je jediné, co odděluje tichou fázi uprostřed praní od ocasu, a jeho snížení může praní zkrátit na polovinu. Ponechte prázdné pro výchozí hodnotu (0,98). Je to jiné nastavení než Poměr chytrého ukončení, které řídí samostatnou kontrolu dřívějšího ukončení.", + "label": "Poměr dokončení ochrany před zmačkáním" + }, "anti_wrinkle_enabled": { "doc": "Rozpoznejte krátké impulzy s nízkým příkonem, které sušič emituje po hlavní tepelné fázi, a udržte je připojené k dokončenému cyklu místo čtení jako nové cykly.", "label": "Povolit detekci ochrany před zmačkáním" @@ -1230,7 +1258,7 @@ "label": "Počet opakování konce" }, "energy_price_entity": { - "doc": "Senzor s aktuální cenou elektřiny za kWh (např. dynamický tarif). Má přednost před statickou cenou níže. Každý cyklus zaznamenává cenu platnou v okamžiku dokončení.", + "doc": "Senzor s aktuální cenou elektřiny za kWh (např. dynamický tarif). Musí to být cena, ne vlastní entita výkonu nebo energie zásuvky: má přednost před statickou cenou níže, takže počítadlo kWh zadané zde účtuje každý cyklus podle stavu měřiče místo podle vašeho tarifu. Když jsou Časově vážené náklady zapnuté, každý cyklus se účtuje cenou platnou v každém okamžiku jeho běhu; jinak se na něj zafixuje cena platná v okamžiku jeho dokončení.", "label": "Entita ceny energie" }, "energy_price_static": { @@ -1326,7 +1354,7 @@ "label": "Spustit HA události pro oznámení" }, "notify_icon": { - "doc": "Volitelná ikona mdi pro oznámení (např. mdi:washing-machine). Nechte prázdné pro výchozí platformy.", + "doc": "Malá ikona zobrazená v oznámení (např. mdi:washing-machine). Android ji vykreslí ve stavovém řádku; iOS ji zobrazí místo ikony aplikace a vyžaduje mobilní aplikaci Home Assistant (companion app) 2026.8 nebo novější. Nechte prázdné pro výchozí ikonu platformy.", "label": "Ikona oznámení" }, "notify_live_chronometer": { @@ -1402,7 +1430,7 @@ "label": "Práh vypnutí napájení" }, "power_sensor": { - "doc": "Entita senzoru hlásící okamžitý příkon ve wattech pro tento spotřebič (např. sensor.pracka_prikon). Veškerá detekce cyklu je založena na tomto signálu.", + "doc": "Entita senzoru hlásící okamžitý příkon ve wattech pro tento spotřebič (např. sensor.washer_power). Veškerá detekce cyklu je založena na tomto signálu.", "label": "Snímač výkonu" }, "profile_duration_tolerance": { @@ -1437,10 +1465,6 @@ "doc": "Sekundy, po které může čerpadlo běžet nepřetržitě, než je označeno jako možná zaseklé (spustí událost zaseknutoého čerpadla).", "label": "Délka zaseknutého čerpadla" }, - "running_dead_zone": { - "doc": "Po zahájení cyklu jsou poklesy příkonu v tomto okně ignorovány. Pračky se plní studenou vodou (pokles blízký 0 W před ohřevem) – bez této ochrany by tato fáze plnění vypadala jako konec cyklu. Toto NEVYNECHÁVÁ data: celý záznam příkonu je zaznamenán od T=0. Engine návrhů měří skutečný vzor startu vašeho stroje a nastavuje tuto hodnotu automaticky.", - "label": "Mrtvá zóna běhu" - }, "sampling_interval": { "doc": "Očekávaný čas mezi čteními senzoru – používá se ke správnému nastavení okna vyhlazení a debounce startu. Každá aktualizace senzoru je zachycena bez ohledu na tuto hodnotu; jen kalibruje následné výpočty. Engine návrhů měří skutečnou kadenci vašeho senzoru z minulých cyklů a nastavuje tuto hodnotu automaticky.", "label": "Interval vzorkování" @@ -1568,13 +1592,17 @@ "label": "Interval výkonového profilu", "doc": "Velikost segmentu pro atribut senzoru power_profile pro daný profil (plochá sada průměrných wattů za slot spotřebovaná externími plánovači jako EMHASS a tibber_prices). Menší segmenty zachovávají krátké výkonové špičky ostré; větší segmenty vyhlazují tvar. Výchozí 15 min. Pouze při načítání; neovlivňuje detekci." }, + "notify_live_silent": { + "label": "Tiché živé aktualizace", + "doc": "Pouze iOS. Aktualizovat živý průběh potichu: každá aktualizace dorazí bez zvuku a vibrací, s nižší prioritou šetřící baterii, takže je iOS může na chvíli seskupit. První aktualizace cyklu (ta, která spustí Live Activity) a oznámení o startu a dokončení upozorní jako obvykle. Vypnutím budete upozorněni na každou aktualizaci." + }, "notify_live_sticky": { "label": "Zachovat živé oznámení po klepnutí", "doc": "Pouze Android. Živé oznámení průběhu nastavte jako trvalé (přilepené), aby jeho klepnutím nedošlo k zavření průběžného vlákna. Vypnuto zachovává výchozí chování, kdy klepnutí oznámení zavře." }, "notify_live_click_action": { - "label": "Cíl klepnutí na živé oznámení", - "doc": "Pouze Android. Kam klepnutí na živé oznámení průběhu otevře (např. /lovelace/laundry nebo celá URL) namísto vstupní stránky aplikace. Ponechte prázdné pro výchozí nastavení." + "label": "Cíl klepnutí na oznámení", + "doc": "Kam vede klepnutí na oznámení z tohoto spotřebiče, v systémech Android i iOS. Prázdné pole otevře panel WashData na tomto spotřebiči. Zadejte cestu k nástěnce (např. /lovelace/laundry) nebo celou URL, pokud má vést jinam, případně „none“, pokud nemá klepnutí otevírat nic." }, "door_opens_at_end": { "label": "Dveře se na konci automaticky otevřou", @@ -1587,6 +1615,14 @@ "profile_evidence_sources": { "label": "Cykly tvořící program", "doc": "Které cykly se používají k sestavení křivky výkonu každého programu a k přiřazení dokončeného cyklu k ní. Když některý druh odškrtnete, přestane ovlivňovat vaše programy, ale nic se nesmaže - cykly zůstanou v seznamu Cykly a stále je lze označit nebo odstranit. Užitečné, pokud nedůvěřujete importovaným datům. Statistiky to neovlivní: vždy počítají jen cykly, které tento spotřebič skutečně provedl. Odškrtnutí všech možností se ignoruje, protože program bez cyklů by nikdy nemohl být přiřazen." + }, + "energy_price_dynamic": { + "label": "Časově vážené náklady", + "doc": "Účtuje každý cyklus cenou platnou v každém okamžiku jeho běhu místo jediné ceny platné v okamžiku jeho dokončení. Týká se pouze entity ceny (statická cena se měnit nemůže). Nevyžaduje žádné další nastavení: cena se sleduje živě po celou dobu cyklu a Zpracování historie v Diagnostice přepočítá náklady starších cyklů z recorderu. Vypnuto = klasické chování, jedna cena zafixovaná na konci." + }, + "curve_preroll_seconds": { + "doc": "Kolik sekund odečtů z neúspěšných pokusů o spuštění může WashData přenést na začátek křivky cyklu. Křivka cyklu obvykle začíná u pokusu o spuštění, který nakonec uspěl, takže u stroje, který to před rozběhnutím několikrát zkusí - volba programu, zámek dveří, první napouštění - chybí v každé křivce první minuty skutečné činnosti. Nastavte tuto hodnotu o něco výše, než je nejdelší takový rozběh vašeho stroje (300 s většině bohatě stačí). 0 funkci vypne. Mějte na paměti, že zapnutí posune zaznamenaný začátek dříve, takže cykly zaznamenané před změnou a po ní nesou pro stejný program různé doby trvání; naučené průměry se budou posouvat, dokud starší cykly nevypadnou z historie.", + "label": "Náběh křivky" } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Rozdělování…", "deleting": "Mazání…", "imported": "Importováno", - "preparing": "Příprava…" + "preparing": "Příprava…", + "unlabelling": "Odznačování…" }, "suggestion": { "both_agree": "WashData doporučuje", @@ -1745,10 +1782,6 @@ "higher": "Potvrzuje pouze vysoce jisté shody; více cyklů zůstane neoznačených", "lower": "Páruje více cyklů; některé mohou být mírně nesprávné" }, - "running_dead_zone": { - "higher": "Ignoruje krátké výkonové špičky během klidu – méně citlivý na šum", - "lower": "Reaguje na kratší výkonové impulzy – zachytí rychlou přechodnou aktivitu" - }, "start_threshold_w": { "higher": "Zabraňuje falešným startům z krátkých výkonových špiček", "lower": "Zachytí programy s nízkým příkonem; může reagovat na rušení" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} čistých cyklů ({pct}%) mělo vnitřní pauzu >60s, která pokračovala; vyžadovat {val} potvrzení konce.", "learning_confidence": "Jistota p05 z {cycles} cyklů označených uživatelem ({p05}); pod touto hodnotou vyžádat ověření.", "match_interval": "Na základě pozorované frekvence aktualizací (medián={median}s) * 10.", + "match_interval_capped": "Omezeno tak, aby se {persistence} po sobě jdoucích shod vešlo do {pct}% nejkratšího programu ({shortest}s); samotná frekvence aktualizací (medián={median}s) by navrhla delší interval.", + "match_interval_floored": "Samotný nejkratší program ({shortest}s) by hodnotu omezil na {cap}s, což je pod minimem {minimum}s, takže zůstává na minimu: série {persistence} po sobě jdoucích shod trvá {budget}s, tedy více než {pct}% tohoto programu. Rozpoznávání probíhá jen ve chvíli, kdy dorazí nové měření (medián={median}s), takže minimum nestojí nic navíc.", "max_duration_ratio": "Na základě délek označených cyklů (p95={p95}).", "min_active": "Na základě minimálního činného výkonu ({min}W) zaznamenaného v posledním cyklu.", "min_duration_ratio": "Ponecháno co nejnižší, aby byl program rozpoznán brzy v cyklu; prahy jistoty a nejednoznačnosti brání předčasnému potvrzení.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Nahrávání zastaveno", "review_saved": "Hodnocení uloženo", "settings_conflicts": "Před uložením opravte zvýrazněné konflikty nastavení.", + "settings_no_changes": "Žádné změny k uložení", "settings_reverted": "Nastavení bylo obnoveno; integrace se znovu načítá", "settings_saved": "Nastavení uloženo; znovunačtení integrace", "suggestions_applied": "Byly použity návrhy; znovunačtení integrace", @@ -1949,7 +1985,11 @@ "export_selective_done": "Export stažen", "import_selective_done": "Importováno {profiles} profilů a {cycles} cyklů", "hist_csv_required": "Nejprve načtěte soubor CSV nebo vložte jeho obsah", - "file_read_failed": "Tento soubor se nepodařilo přečíst" + "file_read_failed": "Tento soubor se nepodařilo přečíst", + "unlabel_done": "Odznačeno cyklů: {count}", + "unlabel_failed": "Odznačení selhalo: {error}", + "odometer_saved": "Celkový počet cyklů uložen", + "odometer_save_failed": "Celkový počet nelze uložit: {error}" }, "trend": { "down": "Klesající trend", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Myčka: tiché sekundy po očekávané době trvání, než se uvolní čekání na závěrečné odčerpání vody", "smart_termination_duration_ratio": "Zlomek očekávané doby trvání přiřazeného programu, kterého musí cyklus dosáhnout, než jej Chytré ukončení může ukončit dříve; snižte jej u strojů závislých na náplni nebo teplotě", + "anti_crease_finalize_ratio": "Zlomek očekávané doby trvání přiřazeného programu, kterého musí cyklus dosáhnout, než lze ukončit ocas otáčení ochrany před zmačkáním; snižte jej u sušiček s dlouhým ocasem otáčení, u praček ponechte výchozí hodnotu", "anti_wrinkle_enabled": "Pohltit impulzy bubnu po hlavní fázi místo čtení jako nové cykly", "anti_wrinkle_exit_power": "Výkon musí mezi impulzy klesnout pod tuto hodnotu, aby ochrana před zmačkáním zůstala aktivní", "anti_wrinkle_idle_timeout": "Povolený klid mezi dvěma impulzy bubnu, než ochrana před zmačkáním skončí", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Fáze 3: vzdálenost polosaturace DDTW; menší = citlivější na tvar (výchozí 30)", "dtw_refine_top_n": "Fáze 3: kandidáti, které DTW přehodnotí; zvyšte na 7-9, pokud správný profil skončí na 4.-5. místě (výchozí 5)", "duration_scale": "Fáze 4: logaritmický poměr, při kterém se shoda délky trvání sníží na polovinu; menší = přísnější penalizace (výchozí 0,175)", - "energy_scale": "Fáze 4: logaritmický poměr, při kterém se shoda energie sníží na polovinu; menší = přísnější penalizace (výchozí 0,25)" + "energy_scale": "Fáze 4: logaritmický poměr, při kterém se shoda energie sníží na polovinu; menší = přísnější penalizace (výchozí 0,25)", + "curve_preroll_seconds": "Sekundy odečtů z neúspěšných pokusů o spuštění přenesené na začátek křivky cyklu; 0 = vypnuto" }, "col": { "profile_tip": "Název odpovídajícího programu. Bez štítku znamená, že na konci cyklu nebyl nalezen žádný profil.", @@ -1998,10 +2040,11 @@ "date_tip": "Datum a čas zahájení cyklu.", "duration_tip": "Celková doba běhu cyklu od začátku do konce.", "energy_tip": "Celková spotřebovaná energie (kWh). Vypočteno integrací výkonu v čase.", - "cost_tip": "Náklady na energii pro tento cyklus, zafixované při dokončení podle ceny platné v té době (energie x cena za kWh). Nastavte cenu v Nastavení, abyste ho vyplnili.", + "cost_tip": "Náklady na energii za tento cyklus, zafixované při dokončení. Když jsou Časově vážené náklady zapnuté, křivka výkonu se účtuje cenou platnou v každém okamžiku; jinak energie x jediná cena platná na konci. Nastavte cenu v Nastavení, aby se hodnota vyplnila.", "confidence_tip": "Spolehlivost shody profilu (0-100%). Jak přesně křivka výkonu cyklu odpovídala identifikovanému programu.", "health_tip": "Zdraví cyklu ML (vyšší = lepší). Kliknutím na cyklus jej prohlédnete a zhodnotíte.", - "flags_tip": "Komunitní příznaky a indikátory kvality dat" + "flags_tip": "Komunitní příznaky a indikátory kvality dat", + "cost_dynamic_tip": "Časově vážené: účtováno cenou platnou v průběhu cyklu (efektivně {price} {cur}/kWh)." }, "stat": { "avg": "prům.", @@ -2028,9 +2071,9 @@ "finished": "Cyklus dosáhl koncového stavu a skončil." }, "store": { + "search_ph2": "Hledat podle značky nebo modelu…", "your_model_tip": "Toto je spotřebič, který jste zadali v Nastavení", "your_model": "Vaše", - "search_brand_ph": "Hledat podle značky…", "programs_count": "Programy: {n}", "browse": "Procházet", "device": "Zařízení", @@ -2075,7 +2118,8 @@ "golden": "Opětovné zpracování: doplňování referenčních", "suggestions": "Opětovné zpracování: návrhy", "ml_training": "Opětovné zpracování: trénování ML", - "health": "Opětovné zpracování: zdraví cyklů" + "health": "Opětovné zpracování: zdraví cyklů", + "costs": "Opětovné zpracování: náklady na energii" }, "pg_sweep": { "optimize": "Optimalizace: {param}" diff --git a/custom_components/ha_washdata/translations/panel/da.json b/custom_components/ha_washdata/translations/panel/da.json index e1762b97..9f732037 100644 --- a/custom_components/ha_washdata/translations/panel/da.json +++ b/custom_components/ha_washdata/translations/panel/da.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Mod slutningen bliver dette program stille i omkring {mins} min og trækker derefter cirka {watts} W i {secs} s, før det er færdigt. Set i {seen} af {measured} målte cyklusser: apparatet gør det ikke ved hver kørsel.", + "quiet_tail": "~{mins} min stille slutfase", "artifact_tip": "{n} registrerede anomalier (f.eks. dør åbnet midt i cyklus) – åben for at se dem på grafen", "auto": "(auto-detekteret)", "built_in_tag": "indbygget", @@ -64,7 +66,7 @@ "overrun": "Løb længere end normalt", "underrun": "Færdig hurtigere end normalt", "poor_fit": "dårlig kvalitet", - "poor_fit_tip": "Inkonsekvent kamphistorik – overvej at genopbygge denne profil", + "poor_fit_tip": "Inkonsekvent matchhistorik: overvej at genopbygge denne profil", "restart_gap_tip": "{n} HA-genstartsgab: strømsporet har et hul", "reviewed": "Gennemgået", "steady": "→ stabil", @@ -77,11 +79,14 @@ "approved": "Godkendt", "awaiting": "Afventer godkendelse", "imported_tip": "Importeret fra fællesskabsbutikken. Bruges kun til matchning, tælles ikke med i statistikken.", + "unmatchable": "kan ikke matches", "not_importable": "ikke her", "exists": "findes", - "backfilled_tip": "Registreret i importeret effekthistorik. Påvirker kun programmatchning, tælles ikke med i statistikken." + "backfilled_tip": "Registreret i importeret effekthistorik. Påvirker kun programmatchning, tælles ikke med i statistikken.", + "armed": "(gælder for næste cyklus)" }, "btn": { + "reset_zoom": "Nulstil zoom", "set_brand_model": "Angiv mærke og model", "refresh_catalog": "Opdater katalog", "add_device": "+ Tilføj enhed", @@ -215,6 +220,8 @@ "trim": "Beskær", "trim_split": "Beskær / Opdel", "undo": "Fortryd", + "unlabel_selected": "Fjern mærkning ({n})", + "unlabel_selected_tip": "Fjerner profilmærkningen fra de valgte cyklusser. De bliver i din historik og kan mærkes igen senere - de påvirker bare ikke længere denne profil.", "use": "Brug", "wipe_all": "Slet alle data", "delete_selected": "Slet valgte ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Anvend bedste", "pg_apply_to_settings": "Gem i indstillinger", "pg_apply_to_settings_tip": "Kopiér de værdier, du har redigeret her, til denne enheds aktive indstillinger", - "search": "Søg", "import": "Importér", "disconnect": "Afbryd forbindelse", "connect_github": "Forbind GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Læs fra Home Assistant", "hist_scan": "Scan efter cyklusser", "hist_import_n": "Importér {n} cyklusser", - "hist_goto_cycles": "Vis mig cyklusserne" + "hist_goto_cycles": "Vis mig cyklusserne", + "save_total_cycles": "Gem total" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Skal være over afbrydereffekttærsklen ({pot} W)", "threshold": "Skal være under stoptærsklen ({stop} W) for at have effekt" }, + "price_entity": { + "power_sensor": "Dette er apparatets Effektsensor, ikke en pris. En prisenhed har forrang over den statiske pris nedenfor, så cyklusser ville blive beregnet til watt pr. kWh - ryd dette felt.", + "energy_sensor": "Dette er apparatets Energimåler, ikke en pris. En prisenhed har forrang over den statiske pris nedenfor, så cyklusser ville blive beregnet til måleraflæsningen pr. kWh - ryd dette felt.", + "device_class": "Denne sensor måler {dc}, ikke en pris pr. kWh. En prisenhed har forrang over den statiske pris nedenfor - ryd dette felt, eller peg det på en tarifsensor.", + "unit": "Denne sensor aflæses i {unit}, hvilket ikke er en pris pr. kWh. En prisenhed har forrang over den statiske pris nedenfor - ryd dette felt, eller peg det på en tarifsensor." + }, "pump_stuck": { "duration": "Skal være kortere end ingen-opdatering-timeout ({to} s) for at fastsat-alarmen udløses inden watchdog stopper cyklussen", "timeout": "Skal overstige pumpe-faststidvarighed ({ps} s) for at alarmen udløses inden cyklussen tvangstopper" @@ -349,7 +362,7 @@ "pg_settings_source": "Indstillingernes kilde", "phase_catalog": "Fasekatalog", "playground": "Legeplads", - "process_history": "Proceshistorie", + "process_history": "Behandl historik", "settings_history": "Indstillingshistorik", "status": "Status", "status_graph": "Statusgraf", @@ -360,12 +373,12 @@ "wipe_history": "Tør historie", "recommendations": "Anbefalinger ({n})", "pg_detection_params": "Registreringsindstillinger", - "pg_timeline": "Hændelsestidslinje", "pg_outcome": "Simuleringsresultat", "pg_across_cycles": "På tværs af dine cyklusser", "community_store": "Fællesskabsbutik", "online_account": "Fællesskabsbutik og onlinefunktioner", - "import_power_history": "Importér effekthistorik" + "import_power_history": "Importér effekthistorik", + "service_status": "Servicestatus" }, "health": { "fair": "Acceptabel matchkvalitet", @@ -373,6 +386,10 @@ "poor": "⚠ Dårlig matchkvalitet" }, "lbl": { + "envelope_position_tip": "Position på det matchede programs egen registrerede kurve, {pct} %. Måles ved at justere denne cyklus ind efter profilen i stedet for at tælle tid, så den holder stik, når en kørsel er længere eller kortere end normalt. Den opdateres, mens apparatet er stille, så den kan halte bagefter under en aktiv fase.", + "envelope_position": "kurve {pct} %", + "aria_playground_chart3": "Interaktiv effektgraf for cyklussen: hold markøren over for at aflæse tid/effekt, rul eller knib for at zoome, træk for at panorere", + "device_unresolved": "Utilgængelig enhed ({id})", "drag_to_resize": "Træk for at ændre størrelse", "actions": "Handlinger", "activity": "Aktivitet", @@ -513,7 +530,7 @@ "profile_index": "profilindeks →", "profile_name": "Profilnavn", "program": "Program", - "program_tip": "Tilsidesæt, hvilket profil der matches med den aktuelle cyklus. Automatisk registrering lader integrationen automatisk vælge den bedste match. Fastgør et bestemt program for at tvinge det til at matche, når automatisk registrering er forkert, eller du ved, hvad der kører.", + "program_tip": "Tilsidesæt, hvilket profil der matches med den aktuelle cyklus. Automatisk registrering lader integrationen automatisk vælge den bedste match. Fastgør et bestemt program for at tvinge det til at matche, når automatisk registrering er forkert, eller du ved, hvad der kører. Vælg et, før du starter apparatet, så anvendes det, så snart den næste cyklus begynder.", "progress": "Fremskridt", "raw_socket": "Rå stikkontakt", "read_only": "Skrivebeskyttet", @@ -548,6 +565,7 @@ "stage4": "Trin 4 – overensstemmelse", "status": "Status", "tail_trim": "Beskær slut (s)", + "tap_to_dismiss": "tryk på grafen for at lukke", "timer_auto_pause": "Auto-pause", "timer_min": "min", "timer_msg_placeholder": "Besked (valgfrit, {device}/{program}/{minutes})", @@ -584,9 +602,7 @@ "pg_time_left_model": "Tid tilbage (model)", "pg_time_left_model_tip": "Modelberegnet resterende tid (faseberegner + ML-blanding), ikke en statisk nedtælling", "phase": "Fase", - "pg_mode_simulate": "Simuler", "pg_mode_history": "Test på historik", - "pg_mode_sweep": "Sweep", "pg_ev_detected": "Registreret", "pg_ev_match": "Match bekræftet", "pg_ev_match_changed": "Match ændret", @@ -616,14 +632,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Mål", "pg_steps": "Trin", - "pg_sweep_2d": "2D-varmekort", - "pg_sweep_param_y": "2. parameter", "pg_obj_match": "Matchnøjagtighed", "pg_obj_endtiming": "Sluttidsnøjagtighed", "pg_obj_falseend": "Falsk-slut-rate", "pg_obj_overrun": "Varighedsafvigelse", "pg_obj_ambiguity": "Tvetydighedsrate", - "pg_best_combo": "bedste kombination", "pg_mode_optimize": "Optimér", "pg_group_matching": "Programgenkendelse", "pg_zoom_hint": "dobbeltklik for at nulstille zoom", @@ -706,7 +719,12 @@ "task_history_import_apply": "Importerer cyklusser", "evidence_real_cycles": "Cyklusser, denne maskine har kørt", "evidence_reference_cycles": "Hentet fra fællesskabsbutikken", - "evidence_backfill_cycles": "Fundet i importeret effekthistorik" + "evidence_backfill_cycles": "Fundet i importeret effekthistorik", + "zoom_hint_touch": "knib for at zoome", + "zoom_hint_touch_zoomed": "knib for at zoome · dobbelttryk for at nulstille", + "cycles_since_service": "{since} / {total} cyklusser", + "total_cycles_run": "cyklusser i alt", + "correct_total_cycles": "Ret totalen" }, "log": { "all_levels": "Alle niveauer", @@ -756,7 +774,7 @@ "merge_cycles": "Flet {n} cyklusser", "new_group": "Ny profilgruppe", "new_phase": "Ny fase", - "process_history_msg": "Kør matching igen, opdater forslag, gentræn ML (hvis aktiveret) og genberegn cyklernes tilstand på tværs af alle gemte cyklusser. Dette kan tage et øjeblik.", + "process_history_msg": "Kør matching igen, opdater forslag, gentræn ML (hvis aktiveret), genberegn cyklussernes omkostninger ud fra registrerede energipriser, og genberegn cyklernes tilstand på tværs af alle gemte cyklusser. Dette kan tage et stykke tid.", "process_history_ok": "Behandl", "process_history_title": "Behandl historik", "process_recording": "Behandl optagelse", @@ -789,6 +807,7 @@ "history_import": "Importér effekthistorik" }, "msg": { + "pg_canvas_empty3": "Vælg en cyklus ovenfor, og tryk på Kør for at simulere den. Hold derefter markøren over for at aflæse værdier, rul eller knib for at zoome, og træk for at panorere.", "tail_trim_hint": "Fjern så mange sekunder fra slutningen", "store_sibling_hint": "Er der intet delt til netop din model? En nært beslægtet model fra samme mærke er normalt et godt udgangspunkt.", "store_declare_appliance": "Fortæl WashData, hvilket apparat du har, så viser denne fane de opsætninger, andre har delt til det. Du kan også skrive et mærke ovenfor for at se dig omkring.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Steg over det normale effektbånd i ~{n}s.", "auto_label_intro": "Tildel profiler til umærkede cyklusser, hvis matchkonfidens rydder tærsklen.", "automations_intro": "WashData udløser {start} / {end}-hændelser og stiller entiteter til rådighed, så notifikationer og handlinger bedst bygges som normale Home Assistant-automatiseringer. Automatiseringer, der bruger denne enhed, vises nedenfor.", - "cleanup_intro": "Hver mærket cyklus overlejret. Sæt kryds ved afvigelser og slet for at rydde op i profilen.", + "cleanup_intro": "Hver mærket cyklus overlejret. Sæt kryds ved afvigelser, og fjern derefter deres mærkning (de bliver i historikken, men påvirker ikke længere denne profil), eller slet dem helt.", "clear_debug_hint": "Fjern gemte fejlretningsdata for at frigøre plads.", "collecting_data": "Indsamler data. Cyklusser der stadig mangler, før finjustering kan starte: {need} ({current}/{min}).", "compare_overlay_profiles": "Overlejringsprofiler (svage)", @@ -901,7 +920,7 @@ "preferences_admin": "Præferencer, panel og adgangskontrol", "preferences_adv": "Præferencer", "prefs_personal": "Disse gælder kun for din Home Assistant-konto.", - "process_history_hint": "Kør matching igen på alle gemte cyklusser, opdater justeringsforslag, gentræn ML-modellerne (hvis aktiveret) og genberegn cyklernes tilstand. Kør dette efter en batch af gennemgange.", + "process_history_hint": "Kør matching igen på alle gemte cyklusser, opdater justeringsforslag, gentræn ML-modellerne (hvis aktiveret), genberegn cyklussernes omkostninger ud fra dine registrerede energipriser, og genberegn cyklernes tilstand. Kør dette efter en batch af gennemgange.", "profile_deleted": "Profil slettet", "profile_poor_health_detail": "Cykler, der er tildelt denne profil, har inkonsekvente former eller lav tillid. Overvej at genopbygge konvolutten eller gennemgå mærkede cyklusser.", "profiles_intro": "Klik på en profil for statistik, faser og oprydning. Gruppér næsten identiske programmer (samme form/varighed, forskellig temperatur eller centrifugering), så du kan matche pålideligt valg mellem dem.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' har en lav pasningsscore - dens seneste cyklusser varierer meget eller matcher svagt. Gennemgå dens cyklusser, eller optag profilen igen, så matchning og tidsestimater forbliver præcise.", "advisory_shape_drift": "'{name}' har afveget markant fra sin oprindelige effektkurve. Apparatet kan have ændret adfærd med tiden (f.eks. kalk, slid). Overvej at optage denne profil igen med nye cyklusser.", "advisory_shape_drift_corr": "'{name}' har afveget markant fra sin oprindelige effektkurve (korrelation {corr}). Apparatet kan have ændret adfærd med tiden (f.eks. kalk, slid). Overvej at optage denne profil igen med nye cyklusser.", + "advisory_unmatchable": "'{name}' kan aldrig matches: der ligger ingen cyklus med effektdata bag den, så WashData har intet at sammenligne en kørende cyklus med. Mærk en cyklus af dette program (eller optag en) for at få den med i matchningen igen.", + "advisory_unmatchable_no_duration": "'{name}' kan aldrig matches: cyklussen bag den har effektdata, men ingen brugbar varighed, så WashData har ingen længde at måle et match op imod. Optag dette program igen, eller angiv dets forventede varighed, for at få det med i matchningen igen.", "pg_no_alerts": "Ingen problemer registreret i denne kørsel.", "pg_override_active": "Bruger dine redigerede indstillinger mod de nuværende", - "pg_history_intro": "Afspil dine seneste cyklusser gennem den rigtige registrering og matching. Rediger en registreringsindstilling i Simuler først for at se en før/efter-sammenligning her.", "pg_history_empty": "Tryk på Kør for at afspille dine cyklusser igen.", "pg_history_summary": "{detected}/{total} registreret · {correct} matchet korrekt", "pg_sweep_no_metric": "Ikke nok data til at score dette mål.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Scanningen mislykkedes.", "hist_scan_expired": "Den scanning er ikke længere tilgængelig. Scan igen.", "hist_import_failed": "Importen mislykkedes.", - "imported_history_readonly": "Registreret i importeret effekthistorik. Den påvirker programmatchning, men tælles ikke med i din statistik og kan ikke beskæres eller opdeles. Mærk den for at give programmet navn." + "imported_history_readonly": "Registreret i importeret effekthistorik. Den påvirker programmatchning, men tælles ikke med i din statistik og kan ikke beskæres eller opdeles. Mærk den for at give programmet navn.", + "odometer_intro": "Denne total kan kun stige. Sletning af en cyklusregistrering ændrer den ikke, og den tæller videre ud over grænsen for den gemte historik, så servicepåmindelser bliver ved med at være korrekte.", + "odometer_correct_hint": "Angiv dette, hvis WashData har registreret en kørsel, der aldrig har fundet sted, eller hvis apparatet allerede havde kørt cyklusser, før WashData blev installeret.", + "no_reminders_set": "Der er endnu ikke angivet nogen servicepåmindelser.", + "toast_program_armed": "Program valgt til næste cyklus: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Opvaskemaskine: stille sekunder efter den forventede varighed, før ventetiden på udpumpning ved cyklusslut frigives", @@ -1082,7 +1106,7 @@ "energy_weight": "Trin 4: hvor stærkt energioverensstemmelse påvirker slutscoren (standard 0,22)", "profile_match_min_duration_ratio": "Trin 1: korteste cykellængde som brøkdel af profilvarighed; standard 0,1 (10%)", "profile_match_max_duration_ratio": "Trin 1: længste cykellængde som brøkdel af profilvarighed; standard 1,5 (150%)", - "keep_min_score": "Trin 2: laveste score for at forblive i kampen; standard 0,1 tillader svage kandidater, senere trin vælger bedst", + "keep_min_score": "Trin 2: minimumsscore for at komme videre; standard 0,1 tillader svage match, senere trin vælger det bedste", "dtw_blend": "Trin 3: 0 = kun kernescore, 1 = kun DTW-score, 0,5 = ligelig blanding (standard)", "dtw_ensemble_w": "Trin 3 (ensemble-tilstand): vægt på skaleret L1 kontra derivativ DTW; standard 0,7 foretrækker niveaubevidst", "dtw_ddtw_scale": "Trin 3: DDTW-halvmætningsafstand; mindre = mere formfølsom (standard 30)", @@ -1093,7 +1117,9 @@ "anti_wrinkle_max_power": "En puls over dette afslutter anti-rynke og åbner en ny cyklus", "anti_wrinkle_enabled": "Opfange tumle-pulserne efter hovedfasen i stedet for at læse dem som nye cyklusser", "anti_wrinkle_max_duration": "En puls længere end dette afslutter anti-rynke og åbner en ny cyklus", - "anti_wrinkle_exit_power": "Effekten skal falde under dette mellem pulserne, for at anti-rynke forbliver aktiv" + "anti_wrinkle_exit_power": "Effekten skal falde under dette mellem pulserne, for at anti-rynke forbliver aktiv", + "anti_crease_finalize_ratio": "Andel af det genkendte programs forventede varighed, som en cyklus skal nå, før anti-rynke-efterløbet må afsluttes; sænk den på tørretumblere med langt efterløb, lad vaskemaskiner stå på standardværdien", + "curve_preroll_seconds": "Sekunders målinger fra afbrudte startforsøg, der tages med forrest i en cyklus' kurve; 0 = fra" }, "phase_desc": { "anti_crease": "Lejlighedsvis korte tumblinger efter færdiggørelse for at reducere rynker.", @@ -1191,6 +1217,10 @@ "label": "Frigivelse efter stille tørrefase", "doc": "Når cyklussen overskrider sin forventede varighed: hvor længe opvaskemaskinen skal forblive stille (under Stoptærskel), før WashData holder op med at vente på en sidste udpumpning og afslutter cyklussen. Hæv den, hvis din maskine har en lang stille tørrefase før en sen sidste udpumpning, der bliver overset - et bredere vindue lader den indlærte varighed følge sæsonudsving (koldere indløbsvand = længere cyklusser) i stedet for at låse sig til det gamle gennemsnit. Den forkorter kun ventetiden i forhold til den interne grænse på 30 minutter for slutspidsen, aldrig forlænger den." }, + "anti_crease_finalize_ratio": { + "label": "Forhold for anti-rynke-afslutning", + "doc": "Hvor langt en cyklus skal være inde i det genkendte programs forventede varighed, før WashData må afgøre, at den lavenergi-tumling, den ser, er anti-rynke-efterløbet, og afslutte cyklussen dér. Den forventede varighed er programmets gennemsnit, så på en tørretumbler, hvis køretid med fugtsensor følger tøjmængden, når de fleste kørsler aldrig 98% af den, og efterløbet bliver i stedet stående til reserve-timeoutet - ofte 15 til 40 minutter efter at maskinen faktisk stoppede. Sænk denne værdi (f.eks. 0,75) på sådan en tørretumbler. Lad vaskemaskiner stå på standardværdien: en vaskemaskine ligger under Maks. anti-rynkeeffekt det meste af sin cyklus, så denne brøkdel er det eneste, der skiller en stille fase midt i vasken fra efterløbet, og en sænkning kan halvere en vask. Lad feltet stå tomt for standarden (0,98). Dette er en anden indstilling end Forhold for smart afslutning, som styrer en separat kontrol for tidlig afslutning." + }, "anti_wrinkle_enabled": { "doc": "Genkend de korte tørretumblerimpulser med lav effekt, som en tørretumbler udsender efter hovedvarmefasen, og hold dem fastgjort til den færdige cyklus i stedet for at læse dem som nye cyklusser.", "label": "Aktivér anti-rynke registrering" @@ -1212,7 +1242,7 @@ "label": "Maks. anti-rynkeeffekt" }, "auto_label_confidence": { - "doc": "Hvis kampresultatet ved cyklusslut er på eller over dette, mærkes programmet automatisk uden nogen bekræftelsesprompt. Hæv den for at kræve større sikkerhed før auto-mærkning; sænk den for at automatisere mere. Fungerer sammen med Learning Confidence under den.", + "doc": "Hvis matchscoren ved cyklusslut er på eller over denne, mærkes programmet automatisk uden nogen bekræftelsesprompt. Hæv den for at kræve større sikkerhed før automatisk mærkning; sænk den for at automatisere mere. Fungerer sammen med Læringskonfidens nedenunder.", "label": "Auto-mærkning konfidens" }, "auto_maintenance": { @@ -1223,6 +1253,10 @@ "doc": "Cykler, der er kortere end dette, kasseres som spøgelsescyklusser (testkørsler, åbning af døren for at tilføje en sok).", "label": "Minimum cyklusvarighed" }, + "curve_preroll_seconds": { + "label": "Kurveoptakt", + "doc": "Hvor mange sekunders målinger fra afbrudte startforsøg WashData må tage med forrest i en cyklus' kurve. En cyklus' kurve begynder normalt ved det startforsøg, der endelig lykkes, så på en maskine, der prøver sig frem et par gange, før den går rigtigt i gang - programvalg, dørlås, første indløb - mangler de første minutters reelle aktivitet i hver kurve. Sæt værdien lidt højere end den længste sådan optakt, din maskine har (300 s er rigeligt for de fleste). 0 slår det fra. Vær opmærksom på, at når det slås til, rykkes den registrerede start tidligere, så cyklusser fra før og efter ændringen bærer forskellige varigheder for det samme program; de indlærte gennemsnit vil glide, indtil de ældre cyklusser er faldet ud." + }, "delay_confirm_seconds": { "doc": "Strømmen skal forblive i standby-båndet så længe, før apparatet behandles som ventende på at starte i stedet for at køre.", "label": "Bekræftelsessekunder" @@ -1260,7 +1294,7 @@ "label": "Slutgentagelsesantal" }, "energy_price_entity": { - "doc": "Sensor med den aktuelle elpris pr. kWh (f.eks. en dynamisk tarif). Har forrang over den statiske pris nedenfor. Hver cyklus fryser den gældende pris, når den er færdig.", + "doc": "Sensor med den aktuelle elpris pr. kWh (f.eks. en dynamisk tarif). Skal være en pris, ikke stikkets egen effekt- eller energienhed: den har forrang over den statiske pris nedenfor, så en kWh-tæller her beregner hver cyklus til måleraflæsningen i stedet for til din tarif. Med Tidsvægtede omkostninger slået til beregnes hver cyklus til den pris, der gjaldt i hvert øjeblik, den kørte; ellers fryses den pris, der gjaldt, da den sluttede, fast på den.", "label": "Energiprisenhed" }, "energy_price_static": { @@ -1288,7 +1322,7 @@ "label": "Inverter ekstern udløser (udløs ved SLUK)" }, "learning_confidence": { - "doc": "Hvis kampresultatet falder mellem dette og Auto-Label Confidence, beder en feedbackmeddelelse dig om at verificere det identificerede program. Under denne score er kampen for usikker til at dukke op. Skal holdes under Auto-label konfidens.", + "doc": "Hvis matchscoren ligger mellem denne og Auto-mærkning konfidens, beder en feedbackmeddelelse dig om at bekræfte det identificerede program. Under denne score er matchet for usikkert til at blive vist. Skal holdes under Auto-mærkning konfidens.", "label": "Læringskonfidens" }, "linked_device": { @@ -1356,7 +1390,7 @@ "label": "Send HA-hændelser til notifikationer" }, "notify_icon": { - "doc": "Valgfrit mdi-ikon for meddelelsen (f.eks. mdi:vaskemaskine). Lad stå tomt for platformens standard.", + "doc": "Det lille ikon, der vises på notifikationen (f.eks. mdi:washing-machine). Android tegner det i statuslinjen; iOS viser det i stedet for appens ikon og kræver companion-appen 2026.8 eller nyere. Lad stå tomt for platformens standard.", "label": "Notifikationsikon" }, "notify_live_chronometer": { @@ -1456,7 +1490,7 @@ "label": "Matchtærskel" }, "profile_unmatch_threshold": { - "doc": "Hvis en direkte midt-cyklus-kamp falder under denne score, slettes den foreløbige identifikation. Hold det lidt under Match Threshold, så et kort dyk i ligheden ikke vender skærmen tilbage til uovertruffen.", + "doc": "Hvis et live-match midt i cyklussen falder under denne score, slettes den foreløbige identifikation. Hold den lidt under Matchtærskel, så et kort dyk i lighed ikke sætter visningen tilbage til uden match.", "label": "Fejlmatchtærskel" }, "progress_reset_delay": { @@ -1467,10 +1501,6 @@ "doc": "Sekunder kan en pumpe køre uafbrudt, før den er markeret som muligvis sidder fast (udløser hændelsen stuck-pumpe).", "label": "Varighed for fastklemt pumpe" }, - "running_dead_zone": { - "doc": "Efter en cyklus starter, ignoreres strømfald i dette vindue. Vaskemaskiner fyldes med koldt vand (falder i nærheden af 0 W før opvarmning) - uden denne beskyttelse ligner påfyldningsfasen en cyklusafslutning. Dette springer IKKE data over: sporet med fuld effekt registreres fra T=0. Forslagsmotoren måler din maskines faktiske opstartsmønster og størrelser dette automatisk.", - "label": "Kørende dødzone" - }, "sampling_interval": { "doc": "Forventet tid mellem sensoraflæsninger - bruges til at dimensionere udjævningsvinduet og starte debounce korrekt. Hver sensoropdatering registreres uanset denne værdi; den kalibrerer kun nedstrømsberegningerne. Forslagsmotoren måler din sensors faktiske kadence fra tidligere cyklusser og indstiller denne automatisk.", "label": "Samplingsinterval" @@ -1598,13 +1628,17 @@ "label": "Effektprofil-interval", "doc": "Bucket-størrelse for effektprofilsensorens attribut power_profile (det flade array af gennemsnitlige watt-værdier pr. slot, der bruges af eksterne planlæggere som EMHASS og tibber_prices). Mindre buckets holder korte effektspidser skarpe; større buckets glatter kurven. Standard 15 min. Kun til visning; påvirker ikke detektering." }, + "notify_live_silent": { + "label": "Lydløse live-opdateringer", + "doc": "Kun iOS. Opdater live-forløbet lydløst: hver opdatering kommer uden lyd eller vibration og med en lavere, batterivenlig prioritet, som iOS kortvarigt kan samle i grupper. Den første opdatering i en cyklus (den, der starter Live-aktiviteten) samt start- og slutnotifikationerne giver stadig besked som normalt. Slå dette fra for at få besked ved hver opdatering." + }, "notify_live_sticky": { "label": "Behold live-notifikation ved tryk", "doc": "Kun Android. Gør live-forløbsnotifikationen vedvarende (sticky), så et tryk ikke afviser den. Slukket: standardadfærd, hvor et tryk afviser notifikationen." }, "notify_live_click_action": { - "label": "Trykdestination for live-notifikation", - "doc": "Kun Android. Hvor et tryk på live-forløbsnotifikationen åbner (f.eks. /lovelace/laundry eller en fuld URL) i stedet for app-startsiden. Lad stå blank for standard." + "label": "Trykdestination for notifikation", + "doc": "Hvor et tryk på en notifikation fra dette apparat åbner, på Android og iOS. Tomt åbner WashData-panelet på dette apparat. Angiv en dashboard-sti (f.eks. /lovelace/laundry) eller en fuld URL for at sende det et andet sted hen, eller \"none\" for slet ingen trykdestination." }, "door_opens_at_end": { "label": "Dør åbner automatisk ved afslutning", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Cyklusser, der former et program", "doc": "Hvilke cyklusser der bruges til at opbygge hvert programs effektkurve og til at matche en afsluttet cyklus mod den. Fravælger du en type, former den ikke længere dine programmer, uden at noget slettes - cyklusserne bliver i din Cykler-liste og kan stadig mærkes eller fjernes. Nyttigt, hvis du ikke stoler på importerede data. Statistikken påvirkes ikke: den tæller altid kun de cyklusser, som denne maskine faktisk har kørt. At fravælge alt ignoreres, da et program uden cyklusser bag sig aldrig kunne matche." + }, + "energy_price_dynamic": { + "label": "Tidsvægtede omkostninger", + "doc": "Beregn hver cyklus til den pris, der gjaldt i hvert øjeblik, den kørte, i stedet for den ene pris, der gjaldt, da den sluttede. Gælder kun for en prisenhed (en statisk pris kan ikke ændre sig). Kræver ingen ekstra opsætning: prisen følges live, mens cyklussen kører, og \"Behandl historik\" under Diagnostik genberegner omkostningerne for ældre cyklusser ud fra recorderen. Fra = den klassiske adfærd, én pris frosset ved slutningen." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Opdeler…", "deleting": "Sletter…", "imported": "Importeret", - "preparing": "Forbereder…" + "preparing": "Forbereder…", + "unlabelling": "Fjerner mærkning…" }, "suggestion": { "both_agree": "WashData anbefaler", @@ -1775,10 +1814,6 @@ "higher": "Kun meget sikre matches bekræftes – flere cyklusser forbliver umærkede", "lower": "Flere cyklusser matches – nogle kan være lidt forkerte" }, - "running_dead_zone": { - "higher": "Korte effekttoppe i tomgang ignoreres – mindre støjfølsom", - "lower": "Reagerer på kortere effektimpulser – opfanger hurtige transienter" - }, "start_threshold_w": { "higher": "Undgår falske starter fra korte effekttoppe", "lower": "Opfanger laveffektsprogrammer – kan aktiveres af støj" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} rene cyklusser ({pct}%) havde en intern pause på >60s, der blev genoptaget; kræver {val} slutbekræftelse(r).", "learning_confidence": "p05-konfidens for {cycles} brugermærkede cyklusser ({p05}); under dette anmodes om verificering.", "match_interval": "Baseret på observeret opdateringskadence (median={median}s) * 10.", + "match_interval_capped": "Begrænset, så {persistence} på hinanden følgende match kan være inden for {pct}% af det korteste program ({shortest}s); opdateringskadencen alene (median={median}s) ville have foreslået et længere interval.", + "match_interval_floored": "Alene det korteste program ({shortest}s) ville begrænse dette til {cap}s, under minimumsgrænsen på {minimum}s, så det holdes der: {persistence} match i træk tager {budget}s, hvilket er mere end {pct}% af det program. Matchning kører kun, når en måling kommer ind (median={median}s), så minimum koster ikke noget.", "max_duration_ratio": "Baseret på varigheder for mærkede cyklusser (p95={p95}).", "min_active": "Baseret på laveste aktive effekt ({min}W) observeret i sidste cyklus.", "min_duration_ratio": "Holdt så lav som muligt, så et program genkendes tidligt i cyklussen; konfidens- og tvetydighedsgrænserne forhindrer forhastede beslutninger.", @@ -1921,6 +1958,7 @@ "recording_stopped": "Optagelsen stoppede", "review_saved": "Anmeldelsen er gemt", "settings_conflicts": "Ret de fremhævede indstillingskonflikter inden lagring.", + "settings_no_changes": "Ingen ændringer at gemme", "settings_reverted": "Indstillinger gendannet; integration genindlæsning", "settings_saved": "Indstillinger gemt; integration genindlæsning", "suggestions_applied": "Forslag anvendt; integration genindlæsning", @@ -1979,7 +2017,11 @@ "export_selective_done": "Eksport downloadet", "import_selective_done": "Importerede {profiles} profil(er) og {cycles} cyklus(ser)", "hist_csv_required": "Indlæs først en CSV-fil eller indsæt dens indhold", - "file_read_failed": "Kunne ikke læse den fil" + "file_read_failed": "Kunne ikke læse den fil", + "unlabel_done": "Fjernede mærkning fra {count} cyklus(ser)", + "unlabel_failed": "Fjernelse af mærkning mislykkedes: {error}", + "odometer_saved": "Samlet antal cyklusser gemt", + "odometer_save_failed": "Kunne ikke gemme totalen: {error}" }, "trend": { "down": "Trend nedad", @@ -1998,10 +2040,11 @@ "date_tip": "Dato og klokkeslæt, hvor cyklussen startede.", "duration_tip": "Samlet kørselstid for cyklussen fra start til slut.", "energy_tip": "Samlet forbrugt energi (kWh). Beregnet ved at integrere effekten over tid.", - "cost_tip": "Energiomkostning for denne cyklus, låst ved afslutningen ud fra den pris, der var gældende da (energi x pris pr. kWh). Angiv en pris under Indstillinger for at udfylde den.", + "cost_tip": "Energiomkostninger for denne cyklus, frosset ved afslutningen. Med Tidsvægtede omkostninger slået til beregnes effektkurven til den pris, der gjaldt i hvert øjeblik, ellers energi x den ene pris, der gjaldt til sidst. Angiv en pris under Indstillinger for at udfylde den.", "confidence_tip": "Profilmatchkonfidens (0-100 %). Hvor tæt cyklussens effektkurve matchede det identificerede program.", "health_tip": "ML-cyklussundhed (højere = bedre). Klik på en cyklus for at inspicere og gennemgå den.", - "flags_tip": "Gennemgangs-, anomali- og kildeflags for cyklussen. Hold musen over et ikon for detaljer." + "flags_tip": "Gennemgangs-, anomali- og kildeflags for cyklussen. Hold musen over et ikon for detaljer.", + "cost_dynamic_tip": "Tidsvægtet: beregnet til den pris, der gjaldt under cyklussen (effektivt {price} {cur}/kWh)." }, "stat": { "avg": "gns.", @@ -2028,9 +2071,9 @@ "finished": "Cyklussen nåede en sluttilstand og stoppede." }, "store": { + "search_ph2": "Søg efter mærke eller model…", "your_model_tip": "Dette er det apparat, du har angivet i Indstillinger", "your_model": "Dit apparat", - "search_brand_ph": "Søg efter mærke…", "programs_count": "Programmer: {n}", "browse": "Gennemse", "device": "Enhed", @@ -2075,7 +2118,8 @@ "golden": "Genbehandling: udfylder referencecyklusser", "suggestions": "Genbehandling: forslag", "ml_training": "Genbehandling: ML-træning", - "health": "Genbehandling: cyklussundhed" + "health": "Genbehandling: cyklussundhed", + "costs": "Genbehandling: energiomkostninger" }, "pg_sweep": { "optimize": "Optimér: {param}" diff --git a/custom_components/ha_washdata/translations/panel/de.json b/custom_components/ha_washdata/translations/panel/de.json index b8b086bf..35edc6fa 100644 --- a/custom_components/ha_washdata/translations/panel/de.json +++ b/custom_components/ha_washdata/translations/panel/de.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Gegen Ende wird dieses Programm für etwa {mins} Min. still und zieht dann noch rund {watts} W für {secs} s, bevor es fertig ist. In {seen} von {measured} gemessenen Zyklen beobachtet: das Gerät macht das nicht bei jedem Durchlauf.", + "quiet_tail": "~{mins} Min. Ruhephase", "artifact_tip": "{n} Anomalien erkannt (z. B. Tür mitten im Zyklus geöffnet) – öffnen Sie sie, um sie in der Grafik anzuzeigen", "auto": "(automatisch erkannt)", "built_in_tag": "eingebaut", @@ -64,7 +66,7 @@ "overrun": "Bin länger gelaufen als gewöhnlich", "underrun": "Schneller als gewöhnlich abgeschlossen", "poor_fit": "schlechte Qualität", - "poor_fit_tip": "Inkonsistenter Spielverlauf – erwägen Sie die Neuerstellung dieses Profils", + "poor_fit_tip": "Inkonsistenter Abgleichverlauf: erwägen Sie, dieses Profil neu zu erstellen", "restart_gap_tip": "{n} HA-Neustart-Lücke: Leistungsverlauf hat eine Lücke", "reviewed": "Bewertet", "steady": "→ stetig", @@ -77,11 +79,14 @@ "approved": "Freigegeben", "awaiting": "Warten auf Freigabe", "imported_tip": "Aus dem Community-Store importiert. Wird nur für den Abgleich verwendet, zählt nicht in die Statistik.", + "unmatchable": "nicht zuordenbar", "not_importable": "hier n. v.", "exists": "vorhanden", - "backfilled_tip": "Im importierten Leistungsverlauf erkannt. Beeinflusst nur die Programmzuordnung, zählt nicht in die Statistik." + "backfilled_tip": "Im importierten Leistungsverlauf erkannt. Beeinflusst nur die Programmzuordnung, zählt nicht in die Statistik.", + "armed": "(gilt für den nächsten Zyklus)" }, "btn": { + "reset_zoom": "Zoom zurücksetzen", "set_brand_model": "Marke und Modell festlegen", "refresh_catalog": "Katalog aktualisieren", "add_device": "+ Gerät hinzufügen", @@ -215,6 +220,8 @@ "trim": "Zuschneiden", "trim_split": "Zuschneiden / Teilen", "undo": "Rückgängig", + "unlabel_selected": "Beschriftung entfernen ({n})", + "unlabel_selected_tip": "Entfernt die Profilbeschriftung von den ausgewählten Zyklen. Sie bleiben in Ihrem Verlauf und können später erneut beschriftet werden - sie beeinflussen dieses Profil dann einfach nicht mehr.", "use": "Verwenden", "wipe_all": "Alle Daten löschen", "delete_selected": "Auswahl löschen ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Beste anwenden", "pg_apply_to_settings": "In Einstellungen speichern", "pg_apply_to_settings_tip": "Die hier bearbeiteten Werte in die aktiven Einstellungen dieses Geräts übernehmen", - "search": "Suchen", "import": "Importieren", "disconnect": "Trennen", "connect_github": "Mit GitHub verbinden", @@ -249,7 +255,8 @@ "hist_read_recorder": "Aus Home Assistant lesen", "hist_scan": "Nach Zyklen suchen", "hist_import_n": "{n} Zyklen importieren", - "hist_goto_cycles": "Zeig mir die Zyklen" + "hist_goto_cycles": "Zeig mir die Zyklen", + "save_total_cycles": "Gesamtzahl speichern" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Muss über dem Abschaltschwellenwert ({pot} W) liegen", "threshold": "Muss unter dem Stop-Schwellenwert ({stop} W) liegen, um wirksam zu sein" }, + "price_entity": { + "power_sensor": "Das ist der Leistungssensor des Geräts, kein Preis. Eine Preis-Entität hat Vorrang vor dem untenstehenden statischen Preis, sodass Zyklen mit Watt pro kWh berechnet würden - dieses Feld leeren.", + "energy_sensor": "Das ist der Energiezähler des Geräts, kein Preis. Eine Preis-Entität hat Vorrang vor dem untenstehenden statischen Preis, sodass Zyklen mit dem Zählerstand pro kWh berechnet würden - dieses Feld leeren.", + "device_class": "Dieser Sensor misst {dc}, keinen Preis pro kWh. Eine Preis-Entität hat Vorrang vor dem untenstehenden statischen Preis - dieses Feld leeren oder auf einen Tarifsensor verweisen.", + "unit": "Dieser Sensor liefert Werte in {unit}, das ist kein Preis pro kWh. Eine Preis-Entität hat Vorrang vor dem untenstehenden statischen Preis - dieses Feld leeren oder auf einen Tarifsensor verweisen." + }, "pump_stuck": { "duration": "Muss kleiner als das Timeout ohne Aktualisierung ({to} s) sein, damit der Festsitzen-Alarm vor dem Watchdog auslöst und den Zyklus beendet", "timeout": "Muss die Pumpen-Stuck-Dauer ({ps} s) überschreiten, damit der Festsitzen-Alarm auslöst, bevor der Zyklus zwangsweise gestoppt wird" @@ -349,7 +362,7 @@ "pg_settings_source": "Herkunft der Einstellungen", "phase_catalog": "Phasenkatalog", "playground": "Testbereich", - "process_history": "Prozesshistorie", + "process_history": "Verlauf verarbeiten", "settings_history": "Einstellungsverlauf", "status": "Status", "status_graph": "Statusdiagramm", @@ -360,12 +373,12 @@ "wipe_history": "Verlauf löschen", "recommendations": "Empfehlungen ({n})", "pg_detection_params": "Erkennungseinstellungen", - "pg_timeline": "Ereignis-Zeitachse", "pg_outcome": "Simulationsergebnis", "pg_across_cycles": "Über deine Zyklen hinweg", "community_store": "Community-Store", "online_account": "Community-Store und Onlinefunktionen", - "import_power_history": "Leistungsverlauf importieren" + "import_power_history": "Leistungsverlauf importieren", + "service_status": "Wartungsstatus" }, "health": { "fair": "Akzeptable Übereinstimmungsqualität", @@ -373,6 +386,10 @@ "poor": "⚠ Schlechte Übereinstimmungsqualität" }, "lbl": { + "envelope_position_tip": "Position auf der aufgezeichneten Kurve des zugeordneten Programms, {pct} %. Ermittelt durch den Abgleich dieses Zyklus mit dem Profil statt durch Zählen der Zeit, daher stimmt sie auch, wenn ein Durchlauf länger oder kürzer als üblich ist. Sie wird aktualisiert, während das Gerät still ist, und kann deshalb in einer aktiven Phase hinterherhinken.", + "envelope_position": "Kurve {pct} %", + "aria_playground_chart3": "Interaktives Leistungsdiagramm des Zyklus: zum Ablesen von Zeit/Leistung mit dem Zeiger darüberfahren, scrollen oder mit zwei Fingern zoomen, zum Verschieben ziehen", + "device_unresolved": "Nicht verfügbares Gerät ({id})", "drag_to_resize": "Zum Ändern der Größe ziehen", "actions": "Aktionen", "activity": "Aktivität", @@ -513,7 +530,7 @@ "profile_index": "Profilindex →", "profile_name": "Profilname", "program": "Programm", - "program_tip": "Überschreibt, welches Profil dem aktuellen Zyklus zugeordnet wird. Die automatische Erkennung lässt die Integration automatisch die beste Übereinstimmung auswählen. Heften Sie ein bestimmtes Programm an, um es zu erzwingen, wenn die automatische Erkennung falsch liegt oder Sie wissen, was läuft.", + "program_tip": "Überschreibt, welches Profil dem aktuellen Zyklus zugeordnet wird. Die automatische Erkennung lässt die Integration automatisch die beste Übereinstimmung auswählen. Heften Sie ein bestimmtes Programm an, um es zu erzwingen, wenn die automatische Erkennung falsch liegt oder Sie wissen, was läuft. Wählen Sie eines aus, bevor Sie das Gerät starten, dann wird es angewendet, sobald der nächste Zyklus beginnt.", "progress": "Fortschritt", "raw_socket": "Rohsignal", "read_only": "Schreibgeschützt", @@ -548,6 +565,7 @@ "stage4": "Stufe 4 – Übereinstimmung", "status": "Status", "tail_trim": "Ende kürzen (s)", + "tap_to_dismiss": "zum Ausblenden auf das Diagramm tippen", "timer_auto_pause": "Automatische Pause", "timer_min": "min", "timer_msg_placeholder": "Nachricht (optional, {device}/{program}/{minutes})", @@ -584,9 +602,7 @@ "pg_time_left_model": "Restzeit (Modell)", "pg_time_left_model_tip": "Modellgeschätzte verbleibende Zeit (Phasenschätzer + ML-Mix), kein statischer Countdown", "phase": "Phase", - "pg_mode_simulate": "Simulieren", "pg_mode_history": "An Verlauf testen", - "pg_mode_sweep": "Durchlauf", "pg_ev_detected": "Erkannt", "pg_ev_match": "Übereinstimmung bestätigt", "pg_ev_match_changed": "Übereinstimmung geändert", @@ -616,14 +632,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Ziel", "pg_steps": "Schritte", - "pg_sweep_2d": "2D-Heatmap", - "pg_sweep_param_y": "2. Parameter", "pg_obj_match": "Zuordnungsgenauigkeit", "pg_obj_endtiming": "Endzeit-Genauigkeit", "pg_obj_falseend": "Falsch-Ende-Rate", "pg_obj_overrun": "Dauerabweichung", "pg_obj_ambiguity": "Mehrdeutigkeitsrate", - "pg_best_combo": "beste Kombination", "pg_mode_optimize": "Optimieren", "pg_group_matching": "Programmzuordnung", "pg_zoom_hint": "Doppelklick zum Zurücksetzen des Zooms", @@ -706,7 +719,12 @@ "task_history_import_apply": "Zyklen werden importiert", "evidence_real_cycles": "Zyklen, die diese Maschine ausgeführt hat", "evidence_reference_cycles": "Aus dem Community-Store heruntergeladen", - "evidence_backfill_cycles": "Im importierten Leistungsverlauf gefunden" + "evidence_backfill_cycles": "Im importierten Leistungsverlauf gefunden", + "zoom_hint_touch": "mit zwei Fingern zoomen", + "zoom_hint_touch_zoomed": "mit zwei Fingern zoomen · Doppeltippen zum Zurücksetzen", + "cycles_since_service": "{since} / {total} Zyklen", + "total_cycles_run": "Zyklen insgesamt", + "correct_total_cycles": "Gesamtzahl korrigieren" }, "log": { "all_levels": "Alle Ebenen", @@ -756,7 +774,7 @@ "merge_cycles": "{n} Zyklen zusammenführen", "new_group": "Neue Profilgruppe", "new_phase": "Neue Phase", - "process_history_msg": "Gleiche Zyklen erneut ab, aktualisiere Vorschläge, trainiere ML neu (falls aktiviert) und berechne den Zykluszustand über alle gespeicherten Zyklen neu. Dies kann einige Zeit dauern.", + "process_history_msg": "Gleiche Zyklen erneut ab, aktualisiere Vorschläge, trainiere ML neu (falls aktiviert), berechne die Zykluskosten anhand der erfassten Energiepreise neu und ermittle den Zykluszustand über alle gespeicherten Zyklen neu. Dies kann einige Zeit dauern.", "process_history_ok": "Verarbeiten", "process_history_title": "Verlauf verarbeiten", "process_recording": "Aufnahme verarbeiten", @@ -789,6 +807,7 @@ "history_import": "Leistungsverlauf importieren" }, "msg": { + "pg_canvas_empty3": "Wähle oben einen Zyklus und drücke auf Ausführen, um ihn zu simulieren. Fahre dann mit dem Zeiger darüber, um Werte abzulesen, scrolle oder zoome mit zwei Fingern und ziehe zum Verschieben.", "tail_trim_hint": "So viele Sekunden vom Ende entfernen", "store_sibling_hint": "Für Ihr genaues Modell ist nichts geteilt? Ein nah verwandtes Modell derselben Marke ist meist ein guter Ausgangspunkt.", "store_declare_appliance": "Teilen Sie WashData mit, welches Gerät Sie besitzen, dann zeigt dieser Tab die Setups, die andere dafür geteilt haben. Sie können oben auch eine Marke eingeben, um sich umzusehen.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Überstieg das übliche Leistungsband für ~{n}s.", "auto_label_intro": "Weisen Sie Profile unbeschrifteten Zyklen zu, deren Übereinstimmungskonfidenz den Schwellenwert überschreitet.", "automations_intro": "WashData löst {start} / {end}-Ereignisse aus und stellt Entitäten bereit, sodass Benachrichtigungen und Aktionen am besten als normale Home Assistant-Automatisierungen eingerichtet werden. Automatisierungen, die dieses Gerät verwenden, werden unten angezeigt.", - "cleanup_intro": "Jeder beschriftete Zyklus überlagert. Markieren Sie Ausreißer und löschen Sie sie, um das Profil zu bereinigen.", + "cleanup_intro": "Jeder beschriftete Zyklus überlagert. Markieren Sie Ausreißer und entfernen Sie dann deren Beschriftung (bleiben im Verlauf, beeinflussen dieses Profil aber nicht mehr) oder löschen Sie sie ganz.", "clear_debug_hint": "Entfernen Sie gespeicherte Debug-Daten, um Speicherplatz freizugeben.", "collecting_data": "Daten werden gesammelt. Noch benötigte Zyklen bis zum Start der Feinabstimmung: {need} ({current}/{min}).", "compare_overlay_profiles": "Overlay-Profile (schwach)", @@ -901,7 +920,7 @@ "preferences_admin": "Einstellungen, Bedienfeld und Zugriffskontrolle", "preferences_adv": "Präferenzen", "prefs_personal": "Diese gelten nur für Ihr Home Assistant-Konto.", - "process_history_hint": "Gleiche alle gespeicherten Zyklen erneut ab, aktualisiere Abstimmungsvorschläge, trainiere ML-Modelle neu (falls aktiviert) und berechne den Zykluszustand neu. Nach einer Stapelüberprüfung ausführen.", + "process_history_hint": "Gleiche alle gespeicherten Zyklen erneut ab, aktualisiere Abstimmungsvorschläge, trainiere ML-Modelle neu (falls aktiviert), berechne die Zykluskosten anhand deiner erfassten Energiepreise neu und ermittle den Zykluszustand neu. Nach einer Stapelüberprüfung ausführen.", "profile_deleted": "Profil gelöscht", "profile_poor_health_detail": "Die diesem Profil zugewiesenen Zyklen weisen inkonsistente Formen oder eine geringe Konfidenz auf. Erwägen Sie, den Umschlag neu zu erstellen oder beschriftete Zyklen zu überprüfen.", "profiles_intro": "Klicken Sie auf ein Profil, um Statistiken, Phasen und Bereinigung anzuzeigen. Gruppieren Sie nahezu identische Programme (gleiche Form/Dauer, unterschiedliche Temperatur oder Schleudergang), sodass bei der Übereinstimmung zuverlässig zwischen ihnen ausgewählt wird.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' hat einen niedrigen Übereinstimmungswert - die letzten Zyklen schwanken stark oder passen nur schwach. Überprüfen Sie die Zyklen oder zeichnen Sie das Profil neu auf, damit Erkennung und Zeitschätzungen genau bleiben.", "advisory_shape_drift": "'{name}' hat sich deutlich vom ursprünglichen Leistungsverlauf entfernt. Das Gerät hat sein Verhalten möglicherweise mit der Zeit verändert (z. B. Verkalkung, Verschleiß). Erwägen Sie, dieses Profil mit aktuellen Zyklen neu aufzuzeichnen.", "advisory_shape_drift_corr": "'{name}' hat sich deutlich vom ursprünglichen Leistungsverlauf entfernt (Korrelation {corr}). Das Gerät hat sein Verhalten möglicherweise mit der Zeit verändert (z. B. Verkalkung, Verschleiß). Erwägen Sie, dieses Profil mit aktuellen Zyklen neu aufzuzeichnen.", + "advisory_unmatchable": "'{name}' kann nie zugeordnet werden: Dahinter steht kein Zyklus mit Leistungsdaten, daher hat WashData nichts, womit es einen laufenden Zyklus vergleichen kann. Beschriften Sie einen Zyklus dieses Programms (oder nehmen Sie einen auf), um es wieder in die Zuordnung aufzunehmen.", + "advisory_unmatchable_no_duration": "'{name}' kann nie zugeordnet werden: Der Zyklus dahinter hat zwar Leistungsdaten, aber keine brauchbare Dauer, also fehlt WashData die Länge, an der es eine Zuordnung ausrichten kann. Nehmen Sie dieses Programm neu auf oder tragen Sie seine erwartete Dauer ein, um es wieder in die Zuordnung aufzunehmen.", "pg_no_alerts": "In diesem Lauf wurden keine Probleme erkannt.", "pg_override_active": "Verwendet Ihre bearbeiteten Einstellungen ggü. den aktuellen", - "pg_history_intro": "Spielen Sie Ihre letzten Zyklen durch die echte Erkennung und Zuordnung ab. Bearbeiten Sie zuerst eine Erkennungseinstellung unter Simulieren, um hier einen Vorher-Nachher-Vergleich zu sehen.", "pg_history_empty": "Drücken Sie Ausführen, um Ihre Zyklen erneut abzuspielen.", "pg_history_summary": "{detected}/{total} erkannt · {correct} korrekt zugeordnet", "pg_sweep_no_metric": "Nicht genügend Daten, um dieses Ziel zu bewerten.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Suche fehlgeschlagen.", "hist_scan_expired": "Diese Suche ist nicht mehr verfügbar. Bitte erneut suchen.", "hist_import_failed": "Import fehlgeschlagen.", - "imported_history_readonly": "Im importierten Leistungsverlauf erkannt. Er beeinflusst die Programmzuordnung, zählt aber nicht in Ihre Statistik und kann nicht zugeschnitten oder geteilt werden. Beschriften Sie ihn, um das Programm zu benennen." + "imported_history_readonly": "Im importierten Leistungsverlauf erkannt. Er beeinflusst die Programmzuordnung, zählt aber nicht in Ihre Statistik und kann nicht zugeschnitten oder geteilt werden. Beschriften Sie ihn, um das Programm zu benennen.", + "odometer_intro": "Diese Gesamtzahl steigt immer nur an. Das Löschen eines Zyklus-Eintrags ändert sie nicht, und sie zählt auch über die Grenze des gespeicherten Verlaufs hinaus weiter, damit Wartungserinnerungen korrekt bleiben.", + "odometer_correct_hint": "Setzen Sie diesen Wert, wenn WashData einen Lauf erfasst hat, der nie stattgefunden hat, oder wenn das Gerät schon vor der Installation von WashData Zyklen absolviert hat.", + "no_reminders_set": "Es sind noch keine Service-Erinnerungen eingerichtet.", + "toast_program_armed": "Programm für den nächsten Zyklus vorgemerkt: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Geschirrspüler: Ruhesekunden nach der erwarteten Dauer, bevor die Wartezeit auf das Abpumpen am Zyklusende freigegeben wird", @@ -1093,7 +1117,9 @@ "anti_wrinkle_max_power": "Ein Impuls darüber beendet den Knitterschutz und öffnet einen neuen Zyklus", "anti_wrinkle_enabled": "Die Trommelimpulse nach der Hauptphase schlucken, statt sie als neue Zyklen zu lesen", "anti_wrinkle_max_duration": "Ein längerer Impuls beendet den Knitterschutz und öffnet einen neuen Zyklus", - "anti_wrinkle_exit_power": "Die Leistung muss zwischen den Impulsen darunter fallen, damit der Knitterschutz aktiv bleibt" + "anti_wrinkle_exit_power": "Die Leistung muss zwischen den Impulsen darunter fallen, damit der Knitterschutz aktiv bleibt", + "anti_crease_finalize_ratio": "Anteil der erwarteten Dauer des zugeordneten Programms, den ein Zyklus erreichen muss, bevor der Knitterschutz-Nachlauf abgeschlossen werden darf; bei Trocknern mit langem Nachlauf senken, Waschmaschinen beim Standard belassen", + "curve_preroll_seconds": "Sekunden an Messwerten aus abgebrochenen Startversuchen, die an den Anfang der Kurve eines Zyklus übernommen werden; 0 = aus" }, "phase_desc": { "anti_crease": "Gelegentliche kurze Trommelumwälzungen nach Abschluss, um Knitterung zu vermeiden.", @@ -1191,6 +1217,10 @@ "label": "Ruhefreigabe nach Passivtrocknung", "doc": "Sobald der Zyklus seine erwartete Dauer überschreitet: wie lange der Geschirrspüler ruhig bleiben muss (unter dem Stoppschwellenwert), bevor WashData nicht mehr auf ein letztes Abpumpen wartet und den Zyklus beendet. Erhöhen Sie den Wert, wenn Ihre Maschine eine lange, stille Trocknungsphase vor einem späten letzten Abpumpen hat, das übersehen wird - ein größeres Fenster lässt die erlernte Dauer der saisonalen Drift folgen (kälteres Zulaufwasser = längere Zyklen), statt sich auf den alten Mittelwert festzulegen. Er verkürzt die Wartezeit gegenüber der internen 30-minütigen Obergrenze für die Endspitze nur, verlängert sie aber nie." }, + "anti_crease_finalize_ratio": { + "label": "Verhältnis für Knitterschutz-Abschluss", + "doc": "Wie weit ein Zyklus in die erwartete Dauer des zugeordneten Programms fortgeschritten sein muss, bevor WashData entscheiden darf, dass das beobachtete Trommeln mit geringer Leistung der Knitterschutz-Nachlauf ist, und den Zyklus dort beenden darf. Die erwartete Dauer ist der Durchschnitt des Programms; bei einem Trockner, dessen Laufzeit mit Feuchtesensor von der Beladung abhängt, erreichen die meisten Läufe daher nie 98 % davon, und der Nachlauf bleibt stattdessen bis zum Rückfall-Timeout stehen - oft 15 bis 40 Minuten, nachdem das Gerät tatsächlich gestoppt hat. Senken Sie den Wert (z. B. 0,75) bei einem solchen Trockner. Belassen Sie Waschmaschinen beim Standard: Eine Waschmaschine liegt während des größten Teils ihres Zyklus unterhalb der Max. Knitterschutzleistung, sodass dieser Anteil das Einzige ist, was eine ruhige Phase mitten im Waschgang vom Nachlauf trennt, und ein Senken kann einen Waschgang halbieren. Leer lassen für den Standard (0,98). Dies ist eine andere Einstellung als das Verhältnis für intelligente Beendigung, das eine separate Prüfung auf vorzeitige Beendigung steuert." + }, "anti_wrinkle_enabled": { "doc": "Erkennen Sie die kurzen, stromsparenden Wäscheimpulse, die ein Trockner nach der Hauptheizphase abgibt, und behalten Sie im Zusammenhang mit dem abgeschlossenen Zyklus bei, anstatt sie als neue Zyklen zu lesen.", "label": "Knitterschutzerkennung aktivieren" @@ -1219,6 +1249,10 @@ "doc": "Zyklen, die kürzer sind, werden als Geisterzyklen (Testläufe, Öffnen der Tür zum Hinzufügen einer Socke) verworfen.", "label": "Mindestzyklusdauer" }, + "curve_preroll_seconds": { + "label": "Kurven-Vorlauf", + "doc": "Wie viele Sekunden an Messwerten aus abgebrochenen Startversuchen WashData an den Anfang der Kurve eines Zyklus übernehmen darf. Die Kurve eines Zyklus beginnt normalerweise bei dem Startversuch, der schließlich erfolgreich ist; bei einem Gerät, das mehrmals antastet, bevor es richtig losläuft - Programmwahl, Türverriegelung, erstes Befüllen -, fehlen daher die ersten Minuten echter Aktivität in jeder Kurve. Stellen Sie diesen Wert etwas höher ein als den längsten solchen Anlauf Ihres Geräts (300 s reichen für die meisten aus). 0 schaltet die Funktion ab. Beachten Sie: Wird sie eingeschaltet, verschiebt sich der aufgezeichnete Start nach vorn, sodass Zyklen von vor und nach der Änderung für dasselbe Programm unterschiedliche Dauern tragen; die gelernten Durchschnittswerte driften, bis die älteren Zyklen herausgefallen sind." + }, "delay_confirm_seconds": { "doc": "Der Strom muss so lange im Standby-Bereich bleiben, bevor das Gerät als „auf den Start wartend“ und nicht als „in Betrieb“ behandelt wird.", "label": "Bestätigungssekunden" @@ -1256,7 +1290,7 @@ "label": "End-Wiederholungsanzahl" }, "energy_price_entity": { - "doc": "Sensor mit dem aktuellen Strompreis pro kWh (z. B. ein dynamischer Tarif). Hat Vorrang vor dem untenstehenden statischen Preis. Jeder Zyklus friert den gültigen Preis ein, wenn er beendet ist.", + "doc": "Sensor mit dem aktuellen Strompreis pro kWh (z. B. ein dynamischer Tarif). Muss ein Preis sein, nicht der Leistungs- oder Energiesensor der Steckdose selbst: Er hat Vorrang vor dem untenstehenden statischen Preis, sodass ein kWh-Zähler an dieser Stelle jeden Zyklus zum Zählerstand statt zu Ihrem Tarif berechnet. Wenn Zeitgewichtete Kosten aktiviert ist, wird jeder Zyklus zu dem Preis berechnet, der in jedem Moment seines Laufs galt; andernfalls wird der bei Zyklusende gültige Preis auf ihm eingefroren.", "label": "Energiepreis-Entität" }, "energy_price_static": { @@ -1284,7 +1318,7 @@ "label": "Externen Auslöser umkehren (bei AUS auslösen)" }, "learning_confidence": { - "doc": "Wenn der Übereinstimmungswert zwischen diesem und dem Auto-Label-Konfidenzwert liegt, werden Sie in einer Feedback-Benachrichtigung aufgefordert, das identifizierte Programm zu überprüfen. Unterhalb dieser Punktzahl ist das Spiel zu unsicher, um an die Oberfläche zu kommen. Muss unterhalb der Auto-Label-Konfidenz gehalten werden.", + "doc": "Wenn der Übereinstimmungswert zwischen diesem Wert und der Automatischen Beschriftungskonfidenz liegt, fordert Sie eine Feedback-Benachrichtigung auf, das erkannte Programm zu bestätigen. Unterhalb dieses Werts ist die Übereinstimmung zu unsicher, um angezeigt zu werden. Muss unterhalb der Automatischen Beschriftungskonfidenz gehalten werden.", "label": "Lernkonfidenz" }, "linked_device": { @@ -1352,7 +1386,7 @@ "label": "HA-Ereignisse für Benachrichtigungen auslösen" }, "notify_icon": { - "doc": "Optionales MDI-Symbol für die Benachrichtigung (z. B. mdi:washing-machine). Für den Plattformstandard lassen Sie das Feld leer.", + "doc": "Das kleine Symbol, das auf der Benachrichtigung angezeigt wird (z. B. mdi:washing-machine). Android zeichnet es in der Statusleiste; iOS zeigt es anstelle des App-Symbols und benötigt dafür die Companion-App 2026.8 oder neuer. Für den Plattformstandard lassen Sie das Feld leer.", "label": "Benachrichtigungssymbol" }, "notify_live_chronometer": { @@ -1452,7 +1486,7 @@ "label": "Abgleichschwellenwert" }, "profile_unmatch_threshold": { - "doc": "Wenn ein Live-Match in der Mitte des Zyklus unter diesen Wert fällt, wird die vorläufige Identifizierung gelöscht. Halten Sie den Wert etwas unter dem Übereinstimmungsschwellenwert, damit ein kurzer Rückgang der Ähnlichkeit nicht dazu führt, dass die Anzeige wieder auf „Unübertroffen“ zurückfällt.", + "doc": "Wenn eine laufende Übereinstimmung mitten im Zyklus unter diesen Wert fällt, wird die vorläufige Identifizierung gelöscht. Halten Sie den Wert etwas unter dem Abgleichschwellenwert, damit ein kurzer Rückgang der Ähnlichkeit die Anzeige nicht wieder auf „Nicht zugeordnet“ zurückspringen lässt.", "label": "Nichtübereinstimmungsschwellenwert" }, "progress_reset_delay": { @@ -1463,10 +1497,6 @@ "doc": "Sekunden lang kann eine Pumpe ununterbrochen laufen, bevor sie als möglicherweise steckengeblieben gekennzeichnet wird (löst das Ereignis „Pumpe steckengeblieben“ aus).", "label": "Dauer der feststeckenden Pumpe" }, - "running_dead_zone": { - "doc": "Nach Beginn eines Zyklus werden Leistungseinbrüche innerhalb dieses Fensters ignoriert. Waschmaschinen füllen sich mit kaltem Wasser (der Wert sinkt vor dem Aufheizen auf nahezu 0 W) – ohne diesen Schutz sieht die Füllphase wie ein Zyklusende aus. Dabei werden KEINE Daten übersprungen: Die vollständige Leistungskurve wird ab T=0 aufgezeichnet. Die Vorschlags-Engine misst das tatsächliche Startmuster Ihrer Maschine und passt dieses automatisch an.", - "label": "Laufende Totzone" - }, "sampling_interval": { "doc": "Erwartete Zeit zwischen Sensorablesungen – wird verwendet, um das Glättungsfenster zu dimensionieren und die Entprellung korrekt zu starten. Unabhängig von diesem Wert wird jede Sensoraktualisierung erfasst; es kalibriert nur die nachgelagerten Berechnungen. Die Vorschlags-Engine misst die tatsächliche Trittfrequenz Ihres Sensors aus vergangenen Zyklen und stellt diese automatisch ein.", "label": "Abtastintervall" @@ -1598,13 +1628,17 @@ "label": "Leistungsprofil-Intervall", "doc": "Bucket-Größe für das Attribut power_profile des profilbezogenen Sensors (ein flaches Array der durchschnittlichen Wattzahl pro Slot, das von externen Planern wie EMHASS und tibber_prices verwendet wird). Kleinere Buckets halten kurze Leistungsspitzen scharf; größere Buckets glätten die Kurve. Standard 15 Min. Nur zur Darstellung; hat keinen Einfluss auf die Erkennung." }, + "notify_live_silent": { + "label": "Stille Live-Aktualisierungen", + "doc": "Nur iOS. Aktualisiert den Live-Fortschritt lautlos: Jede Aktualisierung kommt ohne Ton und ohne Vibration an, mit einer niedrigeren, akkuschonenden Priorität, die iOS kurz bündeln kann. Die erste Aktualisierung eines Zyklus (die die Live-Aktivität startet) sowie die Start- und Endbenachrichtigungen melden sich weiterhin wie gewohnt. Schalten Sie dies aus, um bei jeder Aktualisierung benachrichtigt zu werden." + }, "notify_live_sticky": { "label": "Live-Benachrichtigung beim Antippen behalten", "doc": "Nur Android. Die Live-Fortschrittsbenachrichtigung dauerhaft (sticky) anzeigen, sodass sie beim Antippen nicht ausgeblendet wird. Ausgeschaltet: Standardverhalten, bei dem ein Antippen die Benachrichtigung schließt." }, "notify_live_click_action": { - "label": "Ziel beim Antippen der Live-Benachrichtigung", - "doc": "Nur Android. Wohin das Antippen der Live-Fortschrittsbenachrichtigung führt (z. B. /lovelace/laundry oder eine vollständige URL), statt zur Standard-App-Startseite. Leer lassen für Standard." + "label": "Ziel beim Antippen der Benachrichtigung", + "doc": "Wohin das Antippen einer Benachrichtigung dieses Geräts führt, unter Android und iOS. Leer öffnet das WashData-Panel bei diesem Gerät. Geben Sie einen Dashboard-Pfad (z. B. /lovelace/laundry) oder eine vollständige URL ein, um woandershin zu führen, oder „none“ für gar kein Ziel beim Antippen." }, "door_opens_at_end": { "label": "Tür öffnet sich am Ende automatisch", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Zyklen, die ein Programm prägen", "doc": "Welche Zyklen zum Aufbau der Leistungskurve jedes Programms und zum Abgleich eines abgeschlossenen Zyklus damit verwendet werden. Wird eine Art abgewählt, prägt sie Ihre Programme nicht mehr, ohne dass etwas gelöscht wird - die Zyklen bleiben in Ihrer Zyklusliste und können weiterhin beschriftet oder entfernt werden. Nützlich, wenn Sie importierten Daten nicht vertrauen. Die Statistik ist davon nicht betroffen: Sie zählt immer nur die Zyklen, die diese Maschine wirklich ausgeführt hat. Alles abzuwählen wird ignoriert, da ein Programm ohne Zyklen dahinter nie zugeordnet werden könnte." + }, + "energy_price_dynamic": { + "label": "Zeitgewichtete Kosten", + "doc": "Berechnet jeden Zyklus zu dem Preis, der in jedem Moment seines Laufs galt, statt zu dem einen Preis, der bei Zyklusende aktuell war. Gilt nur für eine Preis-Entität (ein statischer Preis kann sich nicht ändern). Erfordert keine zusätzliche Einrichtung: Während der Zyklus läuft, wird der Preis live mitverfolgt, und „Verlauf verarbeiten“ unter Diagnose berechnet die Kosten älterer Zyklen aus dem Recorder neu. Aus = das klassische Verhalten, ein einzelner am Ende eingefrorener Preis." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Wird geteilt…", "deleting": "Wird gelöscht…", "imported": "Importiert", - "preparing": "Wird vorbereitet…" + "preparing": "Wird vorbereitet…", + "unlabelling": "Beschriftung wird entfernt…" }, "suggestion": { "both_agree": "WashData empfiehlt", @@ -1775,10 +1814,6 @@ "higher": "Nur sehr sichere Übereinstimmungen werden bestätigt – mehr Zyklen bleiben unbeschriftet", "lower": "Mehr Zyklen werden zugeordnet – einzelne könnten leicht falsch sein" }, - "running_dead_zone": { - "higher": "Kurze Leistungsspitzen im Leerlauf werden ignoriert – weniger rauschempfindlich", - "lower": "Reagiert auf kürzere Leistungsimpulse – erfasst schnelle Transienten" - }, "start_threshold_w": { "higher": "Vermeidet Fehlstarts durch kurze Leistungsspitzen", "lower": "Erkennt leistungsschwache Programme – kann auf Rauschen reagieren" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} sauberen Zyklen ({pct}%) hatten eine interne Pause >60s, die wieder fortgesetzt wurde; {val} Endbestätigung(en) erforderlich.", "learning_confidence": "p05-Konfidenz von {cycles} vom Benutzer gelabelten Zyklen ({p05}); darunter wird eine Bestätigung angefordert.", "match_interval": "Basierend auf dem beobachteten Aktualisierungsrhythmus (Median={median}s) * 10.", + "match_interval_capped": "Begrenzt, damit {persistence} aufeinanderfolgende Zuordnungen in {pct}% des kürzesten Programms ({shortest}s) passen; der Aktualisierungsrhythmus allein (Median={median}s) hätte ein längeres Intervall vorgeschlagen.", + "match_interval_floored": "Allein das kürzeste Programm ({shortest}s) würde das hier auf {cap}s begrenzen, unter dem Mindestwert von {minimum}s, deshalb bleibt es dort: {persistence} aufeinanderfolgende Zuordnungen brauchen {budget}s, also mehr als {pct}% dieses Programms. Die Zuordnung läuft nur, wenn ein Messwert eintrifft (Median={median}s), der Mindestwert kostet also nichts.", "max_duration_ratio": "Basierend auf den Dauern gelabelter Zyklen (p95={p95}).", "min_active": "Basierend auf der minimalen aktiven Leistung ({min}W), die im letzten Zyklus beobachtet wurde.", "min_duration_ratio": "So niedrig wie möglich gehalten, damit ein Programm früh im Zyklus erkannt wird; die Konfidenz- und Mehrdeutigkeitsschranken verhindern vorzeitige Festlegungen.", @@ -1921,6 +1958,7 @@ "recording_stopped": "Die Aufnahme wurde gestoppt", "review_saved": "Bewertung gespeichert", "settings_conflicts": "Beheben Sie die markierten Einstellungskonflikte, bevor Sie speichern.", + "settings_no_changes": "Keine Änderungen zum Speichern", "settings_reverted": "Einstellungen wiederhergestellt; Neuladen der Integration", "settings_saved": "Einstellungen gespeichert; Neuladen der Integration", "suggestions_applied": "Angewendete Vorschläge; Neuladen der Integration", @@ -1979,7 +2017,11 @@ "export_selective_done": "Export heruntergeladen", "import_selective_done": "{profiles} Profil(e) und {cycles} Zyklus/Zyklen importiert", "hist_csv_required": "Laden Sie zuerst eine CSV-Datei oder fügen Sie deren Inhalt ein", - "file_read_failed": "Diese Datei konnte nicht gelesen werden" + "file_read_failed": "Diese Datei konnte nicht gelesen werden", + "unlabel_done": "Beschriftung von {count} Zyklen entfernt", + "unlabel_failed": "Entfernen der Beschriftung fehlgeschlagen: {error}", + "odometer_saved": "Gesamtzahl der Zyklen gespeichert", + "odometer_save_failed": "Gesamtzahl konnte nicht gespeichert werden: {error}" }, "trend": { "down": "Abwärtstrend", @@ -1998,10 +2040,11 @@ "date_tip": "Datum und Uhrzeit, zu der der Zyklus gestartet ist.", "duration_tip": "Gesamtlaufzeit des Zyklus von Anfang bis Ende.", "energy_tip": "Insgesamt verbrauchte Energie (kWh). Berechnet durch Integration der Leistung über die Zeit.", - "cost_tip": "Energiekosten für diesen Zyklus, beim Abschluss anhand des damals gültigen Preises festgeschrieben (Energie x Preis pro kWh). Legen Sie unter Einstellungen einen Preis fest, um sie anzuzeigen.", + "cost_tip": "Energiekosten dieses Zyklus, bei Abschluss eingefroren. Wenn Zeitgewichtete Kosten aktiviert ist, wird der Leistungsverlauf zu dem jeweils gültigen Preis berechnet, andernfalls Energie x der einzelne Preis, der am Ende galt. Legen Sie unter Einstellungen einen Preis fest, damit hier Werte erscheinen.", "confidence_tip": "Profilübereinstimmungskonfidenz (0-100 %). Wie genau die Leistungskurve des Zyklus mit dem identifizierten Programm übereinstimmte.", "health_tip": "ML-Zyklusgesundheit (höher = besser). Klicken Sie auf einen Zyklus, um ihn zu prüfen und zu überprüfen.", - "flags_tip": "Überprüfungs-, Anomalie- und Quell-Flags für den Zyklus. Fahren Sie mit dem Zeiger über ein Symbol, um Details anzuzeigen." + "flags_tip": "Überprüfungs-, Anomalie- und Quell-Flags für den Zyklus. Fahren Sie mit dem Zeiger über ein Symbol, um Details anzuzeigen.", + "cost_dynamic_tip": "Zeitgewichtet: zu dem während des Zyklus jeweils gültigen Preis berechnet (effektiv {price} {cur}/kWh)." }, "stat": { "avg": "Ø", @@ -2028,9 +2071,9 @@ "finished": "Der Zyklus hat einen Endzustand erreicht und wurde beendet." }, "store": { + "search_ph2": "Nach Marke oder Modell suchen…", "your_model_tip": "Dies ist das Gerät, das Sie in den Einstellungen angegeben haben", "your_model": "Ihr Gerät", - "search_brand_ph": "Nach Marke suchen…", "programs_count": "Programme: {n}", "browse": "Durchsuchen", "device": "Gerät", @@ -2075,7 +2118,8 @@ "golden": "Neuverarbeitung: Referenzzyklen nachtragen", "suggestions": "Neuverarbeitung: Vorschläge", "ml_training": "Neuverarbeitung: ML-Training", - "health": "Neuverarbeitung: Zyklusgesundheit" + "health": "Neuverarbeitung: Zyklusgesundheit", + "costs": "Neuverarbeitung: Energiekosten" }, "pg_sweep": { "optimize": "Optimieren: {param}" diff --git a/custom_components/ha_washdata/translations/panel/el.json b/custom_components/ha_washdata/translations/panel/el.json index 7380bded..e21bb381 100644 --- a/custom_components/ha_washdata/translations/panel/el.json +++ b/custom_components/ha_washdata/translations/panel/el.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Κοντά στο τέλος αυτό το πρόγραμμα ησυχάζει για περίπου {mins} λεπτ., έπειτα τραβά περίπου {watts} W για {secs} δευτ. και ολοκληρώνεται. Παρατηρήθηκε σε {seen}/{measured} μετρημένους κύκλους: η συσκευή δεν το κάνει σε κάθε εκτέλεση.", + "quiet_tail": "~{mins} λεπτ. ησυχίας στο τέλος", "artifact_tip": "Εντοπίστηκαν {n} ανωμαλίες (π.χ. η πόρτα άνοιξε στα μέσα του κύκλου) – ανοίξτε για να τις δείτε στο γράφημα", "auto": "(αυτόματη ανίχνευση)", "built_in_tag": "ενσωματωμένο", @@ -77,11 +79,14 @@ "approved": "Εγκρίθηκε", "awaiting": "Αναμονή έγκρισης", "imported_tip": "Εισαγωγή από το κοινοτικό κατάστημα. Χρησιμοποιείται μόνο για αντιστοίχιση, δεν προσμετράται στα στατιστικά.", + "unmatchable": "δεν αντιστοιχίζεται", "not_importable": "μη διαθέσιμο εδώ", "exists": "υπάρχει", - "backfilled_tip": "Ανιχνεύθηκε σε εισαγόμενο ιστορικό ισχύος. Διαμορφώνει μόνο την αντιστοίχιση προγραμμάτων, δεν προσμετράται στα στατιστικά." + "backfilled_tip": "Ανιχνεύθηκε σε εισαγόμενο ιστορικό ισχύος. Διαμορφώνει μόνο την αντιστοίχιση προγραμμάτων, δεν προσμετράται στα στατιστικά.", + "armed": "(εφαρμόζεται στον επόμενο κύκλο)" }, "btn": { + "reset_zoom": "Επαναφορά ζουμ", "set_brand_model": "Ορισμός μάρκας και μοντέλου", "refresh_catalog": "Ανανέωση καταλόγου", "add_device": "+ Προσθήκη συσκευής", @@ -215,6 +220,8 @@ "trim": "Περικοπή", "trim_split": "Περικοπή / Διαχωρισμός", "undo": "Αναίρεση", + "unlabel_selected": "Αφαίρεση επισήμανσης επιλεγμένων ({n})", + "unlabel_selected_tip": "Αφαιρεί την ετικέτα προφίλ από τους επιλεγμένους κύκλους. Παραμένουν στο ιστορικό σας και μπορούν να επισημανθούν ξανά αργότερα - απλώς παύουν να διαμορφώνουν αυτό το προφίλ.", "use": "Χρήση", "wipe_all": "Διαγραφή όλων των δεδομένων", "delete_selected": "Διαγραφή επιλεγμένων ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Εφαρμογή καλύτερου", "pg_apply_to_settings": "Αποθήκευση στις ρυθμίσεις", "pg_apply_to_settings_tip": "Αντιγραφή των τιμών που επεξεργαστήκατε εδώ στις ενεργές ρυθμίσεις αυτής της συσκευής", - "search": "Αναζήτηση", "import": "Εισαγωγή", "disconnect": "Αποσύνδεση", "connect_github": "Σύνδεση GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Ανάγνωση από το Home Assistant", "hist_scan": "Σάρωση για κύκλους", "hist_import_n": "Εισαγωγή {n} κύκλων", - "hist_goto_cycles": "Εμφάνιση κύκλων" + "hist_goto_cycles": "Εμφάνιση κύκλων", + "save_total_cycles": "Αποθήκευση συνόλου" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Διορθώστε τις συγκρούσεις πριν αποθηκεύσετε", "attn_title": "Συγκρούσεις ρυθμίσεων: {n}", + "price_entity": { + "power_sensor": "Αυτός είναι ο Αισθητήρας Ισχύος της συσκευής, όχι τιμή. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω, οπότε οι κύκλοι θα κοστολογούνταν με την ένδειξη σε watt ως τιμή ανά kWh - καθαρίστε αυτό το πεδίο.", + "energy_sensor": "Αυτός είναι ο Μετρητής Ενέργειας της συσκευής, όχι τιμή. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω, οπότε οι κύκλοι θα κοστολογούνταν με την ένδειξη του μετρητή ως τιμή ανά kWh - καθαρίστε αυτό το πεδίο.", + "device_class": "Αυτός ο αισθητήρας μετρά {dc}, όχι τιμή ανά kWh. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω - καθαρίστε αυτό το πεδίο ή επιλέξτε έναν αισθητήρα τιμολογίου.", + "unit": "Αυτός ο αισθητήρας δίνει ένδειξη σε {unit}, που δεν είναι τιμή ανά kWh. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω - καθαρίστε αυτό το πεδίο ή επιλέξτε έναν αισθητήρα τιμολογίου." + }, "settings_banner": "Συγκρούσεις ρυθμίσεων: {n}. Ελέγξτε τα επισημασμένα τμήματα και διορθώστε τα πριν την αποθήκευση.", "settings_banner_btn": "Μετάβαση στο πρώτο", "confidence": { @@ -360,12 +373,12 @@ "wipe_history": "Διαγραφή ιστορικού", "recommendations": "Συστάσεις ({n})", "pg_detection_params": "Ρυθμίσεις ανίχνευσης", - "pg_timeline": "Χρονολόγιο συμβάντων", "pg_outcome": "Αποτέλεσμα προσομοίωσης", "pg_across_cycles": "Σε όλους τους κύκλους σας", "community_store": "Κοινοτικό Κατάστημα", "online_account": "Κοινοτικό Κατάστημα & διαδικτυακές λειτουργίες", - "import_power_history": "Εισαγωγή ιστορικού ισχύος" + "import_power_history": "Εισαγωγή ιστορικού ισχύος", + "service_status": "Κατάσταση συντήρησης" }, "health": { "fair": "Αποδεκτή ποιότητα αντιστοίχισης", @@ -373,6 +386,10 @@ "poor": "⚠ Κακή ποιότητα αντιστοίχισης" }, "lbl": { + "envelope_position_tip": "Θέση πάνω στην καταγεγραμμένη καμπύλη του ίδιου του αντιστοιχισμένου προγράμματος, {pct}%. Μετριέται με ευθυγράμμιση αυτού του κύκλου με το προφίλ αντί για μέτρηση του χρόνου, γι' αυτό παραμένει σωστή όταν μια εκτέλεση είναι μεγαλύτερη ή μικρότερη από τη συνηθισμένη. Ανανεώνεται όσο η συσκευή είναι ήσυχη, γι' αυτό μπορεί να καθυστερεί κατά τη διάρκεια μιας ενεργής φάσης.", + "envelope_position": "καμπύλη {pct}%", + "aria_playground_chart3": "Διαδραστικό γράφημα ισχύος κύκλου: τοποθετήστε τον δείκτη για ανάγνωση χρόνου/ισχύος, κυλήστε ή κάντε ζουμ με δύο δάχτυλα, σύρετε για μετακίνηση", + "device_unresolved": "Μη διαθέσιμη συσκευή ({id})", "drag_to_resize": "Σύρετε για αλλαγή μεγέθους", "actions": "Ενέργειες", "activity": "Δραστηριότητα", @@ -508,7 +525,7 @@ "profile_index": "δείκτης προφίλ →", "profile_name": "Όνομα προφίλ", "program": "Πρόγραμμα", - "program_tip": "Παρακάμψτε ποιο προφίλ αντιστοιχίζεται στον τρέχοντα κύκλο. Η αυτόματη ανίχνευση επιτρέπει στην ενσωμάτωση να επιλέξει αυτόματα την καλύτερη αντιστοίχιση. Καρφιτσώστε ένα συγκεκριμένο πρόγραμμα για αναγκαστική αντιστοίχιση όταν η αυτόματη ανίχνευση είναι λανθασμένη ή γνωρίζετε τι εκτελείται.", + "program_tip": "Παρακάμψτε ποιο προφίλ αντιστοιχίζεται στον τρέχοντα κύκλο. Η αυτόματη ανίχνευση επιτρέπει στην ενσωμάτωση να επιλέξει αυτόματα την καλύτερη αντιστοίχιση. Καρφιτσώστε ένα συγκεκριμένο πρόγραμμα για αναγκαστική αντιστοίχιση όταν η αυτόματη ανίχνευση είναι λανθασμένη ή γνωρίζετε τι εκτελείται. Επιλέξτε ένα πρόγραμμα πριν θέσετε σε λειτουργία τη συσκευή και θα εφαρμοστεί μόλις ξεκινήσει ο επόμενος κύκλος.", "progress": "Πρόοδος", "raw_socket": "Ακατέργαστο σήμα πρίζας", "read_only": "Μόνο για ανάγνωση", @@ -543,6 +560,7 @@ "stage4": "Στάδιο 4 – συμφωνία", "status": "Κατάσταση", "tail_trim": "Περικοπή τέλους (δευτ.)", + "tap_to_dismiss": "πατήστε το γράφημα για κλείσιμο", "timer_auto_pause": "Αυτόματη παύση", "timer_min": "λεπτά", "timer_msg_placeholder": "Μήνυμα (προαιρετικό, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Χρόνος που απομένει (μοντέλο)", "pg_time_left_model_tip": "Εκτιμώμενος από το μοντέλο χρόνος που απομένει (εκτιμητής φάσης + μείγμα ML), όχι στατική αντίστροφη μέτρηση", "phase": "Φάση", - "pg_mode_simulate": "Προσομοίωση", "pg_mode_history": "Δοκιμή στο ιστορικό", - "pg_mode_sweep": "Σάρωση", "pg_ev_detected": "Ανιχνεύθηκε", "pg_ev_match": "Αντιστοίχιση οριστικοποιήθηκε", "pg_ev_match_changed": "Αντιστοίχιση άλλαξε", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Παρακάμψτε το αυτόματα ανιχνευθέν κατώφλι κατάστασης αναμονής (αφήστε κενό για αυτόματο).", "pg_stress_toggle": "Δοκιμή τερματισμού αδράνειας", "pg_stress_toggle_desc": "Προσομοιώνει τη συσκευή που παραμένει στο επίπεδο κατανάλωσης αναμονής μετά το τέλος της εγγραφής - δείχνει αν και πότε το WashData σταματά τον κύκλο.", - "pg_sweep_2d": "Χάρτης θερμότητας 2D", - "pg_sweep_param_y": "2η παράμετρος", "pg_obj_match": "Ακρίβεια αντιστοίχισης", "pg_obj_endtiming": "Ακρίβεια χρόνου λήξης", "pg_obj_falseend": "Ποσοστό ψευδών λήξεων", "pg_obj_overrun": "Απόκλιση διάρκειας", "pg_obj_ambiguity": "Ποσοστό ασάφειας", - "pg_best_combo": "καλύτερος συνδυασμός", "pg_mode_optimize": "Βελτιστοποίηση", "pg_group_matching": "Αντιστοίχιση προγράμματος", "pg_zoom_hint": "διπλό κλικ για επαναφορά ζουμ", @@ -706,7 +719,12 @@ "task_history_import_apply": "Εισαγωγή κύκλων", "evidence_real_cycles": "Κύκλοι που εκτέλεσε αυτό το μηχάνημα", "evidence_reference_cycles": "Λήψη από το κοινοτικό κατάστημα", - "evidence_backfill_cycles": "Βρέθηκαν σε εισαγόμενο ιστορικό ισχύος" + "evidence_backfill_cycles": "Βρέθηκαν σε εισαγόμενο ιστορικό ισχύος", + "zoom_hint_touch": "ζουμ με δύο δάχτυλα", + "zoom_hint_touch_zoomed": "ζουμ με δύο δάχτυλα · διπλό πάτημα για επαναφορά", + "cycles_since_service": "{since} / {total} κύκλοι", + "total_cycles_run": "κύκλοι συνολικά", + "correct_total_cycles": "Διόρθωση συνόλου" }, "log": { "all_levels": "Όλα τα επίπεδα", @@ -756,7 +774,7 @@ "merge_cycles": "Συγχώνευση {n} Κύκλων", "new_group": "Νέα ομάδα προφίλ", "new_phase": "Νέα Φάση", - "process_history_msg": "Εκτέλεση αντιστοίχισης, ανανέωση προτάσεων, επανεκπαίδευση ML (αν είναι ενεργοποιημένο) και επανυπολογισμός υγείας κύκλων σε όλους τους αποθηκευμένους κύκλους. Αυτό μπορεί να πάρει λίγη ώρα.", + "process_history_msg": "Εκτέλεση αντιστοίχισης, ανανέωση προτάσεων, επανεκπαίδευση ML (αν είναι ενεργοποιημένο), επανυπολογισμός κόστους των κύκλων με βάση τις καταγεγραμμένες τιμές ενέργειας και επανυπολογισμός υγείας κύκλων σε όλους τους αποθηκευμένους κύκλους. Αυτό μπορεί να πάρει λίγη ώρα.", "process_history_ok": "Επεξεργασία", "process_history_title": "Επεξεργασία Ιστορικού", "process_recording": "Διαδικασία Εγγραφής", @@ -789,6 +807,7 @@ "history_import": "Εισαγωγή ιστορικού ισχύος" }, "msg": { + "pg_canvas_empty3": "Επιλέξτε έναν κύκλο παραπάνω και πατήστε Εκτέλεση για να τον προσομοιώσετε. Στη συνέχεια τοποθετήστε τον δείκτη για ανάγνωση τιμών, κυλήστε ή κάντε ζουμ με δύο δάχτυλα και σύρετε για μετακίνηση.", "tail_trim_hint": "Αφαιρέστε τόσα δευτερόλεπτα από το τέλος", "store_sibling_hint": "Δεν υπάρχει τίποτα κοινοποιημένο για το ακριβές μοντέλο σας; Ένα στενά συγγενικό μοντέλο της ίδιας μάρκας είναι συνήθως καλή αφετηρία.", "store_declare_appliance": "Δηλώστε στο WashData ποια συσκευή έχετε και αυτή η καρτέλα θα εμφανίσει τις διαμορφώσεις που έχουν κοινοποιήσει άλλοι για αυτήν. Μπορείτε επίσης να γράψετε μια μάρκα παραπάνω για να δείτε τον κατάλογο.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Υπερέβη το συνηθισμένο εύρος ισχύος για ~{n}s.", "auto_label_intro": "Εκχωρήστε προφίλ σε κύκλους χωρίς ετικέτα, των οποίων η εμπιστοσύνη αντιστοίχισης εξαλείφει το όριο.", "automations_intro": "Το WashData ενεργοποιεί συμβάντα {start} / {end} και εκθέτει οντότητες, οπότε οι ειδοποιήσεις και οι ενέργειες κατασκευάζονται καλύτερα ως κανονικοί αυτοματισμοί του Home Assistant. Οι αυτοματισμοί που χρησιμοποιούν αυτή τη συσκευή εμφανίζονται παρακάτω.", - "cleanup_intro": "Κάθε κύκλος με ετικέτα επικαλύπτεται. Σημειώστε ακραίες τιμές και διαγράψτε για να καθαρίσετε το προφίλ.", + "cleanup_intro": "Όλοι οι κύκλοι με ετικέτα επικαλύπτονται. Σημειώστε τις ακραίες τιμές και αφαιρέστε την επισήμανσή τους (παραμένουν στο ιστορικό, αλλά δεν διαμορφώνουν πλέον αυτό το προφίλ) ή διαγράψτε τους οριστικά.", "clear_debug_hint": "Καταργήστε τα αποθηκευμένα δεδομένα εντοπισμού σφαλμάτων για να ελευθερώσετε χώρο.", "collecting_data": "Συλλογή δεδομένων. Κύκλοι που απαιτούνται ακόμη πριν ξεκινήσει η βελτιστοποίηση: {need} ({current}/{min}).", "compare_overlay_profiles": "Προφίλ επικάλυψης (ασθενώς)", @@ -853,7 +872,7 @@ "no_fine_tuned": "Τίποτα δεν έχει βελτιωθεί ακόμα – Η WashData χρησιμοποιεί τα ενσωματωμένα μοντέλα της.", "no_logs": "Δεν υπάρχουν ακόμα εγγραφές καταγραφής στην προσωρινή μνήμη.", "no_maintenance": "Δεν έχει καταγραφεί συντήρηση ακόμα.", - "no_match_yet": "Καμία προσπάθεια αγώνα ακόμα - συμπληρώνεται κατά τη διάρκεια ενός κύκλου τρεξίματος.", + "no_match_yet": "Καμία προσπάθεια αντιστοίχισης ακόμη - συμπληρώνεται κατά τη διάρκεια ενός κύκλου σε εξέλιξη.", "no_other_users": "Δεν βρέθηκαν άλλοι χρήστες του Home Assistant.", "no_phases": "Δεν έχουν καθοριστεί φάσεις.", "no_phases_assigned": "Δεν έχουν εκχωρηθεί φάσεις.", @@ -901,7 +920,7 @@ "preferences_admin": "Προτιμήσεις, πίνακας & έλεγχος πρόσβασης", "preferences_adv": "Προτιμήσεις", "prefs_personal": "Αυτά ισχύουν μόνο για τον λογαριασμό σας Home Assistant.", - "process_history_hint": "Εκτέλεση αντιστοίχισης σε όλους τους αποθηκευμένους κύκλους, ανανέωση προτάσεων ρύθμισης, επανεκπαίδευση μοντέλων ML (αν είναι ενεργοποιημένα) και επανυπολογισμός υγείας κύκλων. Εκτελέστε το μετά από μια δέσμη αξιολογήσεων.", + "process_history_hint": "Εκτέλεση αντιστοίχισης σε όλους τους αποθηκευμένους κύκλους, ανανέωση προτάσεων ρύθμισης, επανεκπαίδευση μοντέλων ML (αν είναι ενεργοποιημένα), επανυπολογισμός κόστους των κύκλων με βάση τις καταγεγραμμένες τιμές ενέργειας και επανυπολογισμός υγείας κύκλων. Εκτελέστε το μετά από μια δέσμη αξιολογήσεων.", "profile_deleted": "Το προφίλ διαγράφηκε", "profile_poor_health_detail": "Οι κύκλοι που έχουν εκχωρηθεί σε αυτό το προφίλ έχουν ασυνεπή σχήματα ή χαμηλή εμπιστοσύνη. Εξετάστε το ενδεχόμενο να φτιάξετε ξανά το φάκελο ή να αναθεωρήσετε τους κύκλους με ετικέτα.", "profiles_intro": "Κάντε κλικ σε ένα προφίλ για στατιστικά στοιχεία, φάσεις και εκκαθάριση. Ομαδοποιήστε σχεδόν πανομοιότυπα προγράμματα (ίδιο σχήμα/διάρκεια, διαφορετική θερμοκρασία ή στύψιμο) ώστε να ταιριάζουν αξιόπιστα οι επιλογές μεταξύ τους.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "Το '{name}' έχει χαμηλή βαθμολογία ταιριάσματος - οι πρόσφατοι κύκλοι του διαφέρουν πολύ ή ταιριάζουν ασθενώς. Ελέγξτε τους κύκλους του ή καταγράψτε ξανά το προφίλ ώστε η αναγνώριση και οι εκτιμήσεις χρόνου να παραμείνουν ακριβείς.", "advisory_shape_drift": "Το '{name}' έχει αποκλίνει σημαντικά από την αρχική του καμπύλη ισχύος. Η συμπεριφορά της συσκευής μπορεί να έχει αλλάξει με την πάροδο του χρόνου (π.χ. άλατα, φθορά). Εξετάστε το ενδεχόμενο νέας καταγραφής αυτού του προφίλ με πρόσφατους κύκλους.", "advisory_shape_drift_corr": "Το '{name}' έχει αποκλίνει σημαντικά από την αρχική του καμπύλη ισχύος (συσχέτιση {corr}). Η συμπεριφορά της συσκευής μπορεί να έχει αλλάξει με την πάροδο του χρόνου (π.χ. άλατα, φθορά). Εξετάστε το ενδεχόμενο νέας καταγραφής αυτού του προφίλ με πρόσφατους κύκλους.", + "advisory_unmatchable": "Το '{name}' δεν μπορεί ποτέ να αντιστοιχιστεί: δεν υπάρχει πίσω του κανένας κύκλος με δεδομένα ισχύος, επομένως το WashData δεν έχει με τι να συγκρίνει τον κύκλο που εκτελείται. Επισημάνετε έναν κύκλο αυτού του προγράμματος (ή εγγράψτε έναν) για να επανέλθει στην αντιστοίχιση.", + "advisory_unmatchable_no_duration": "Το '{name}' δεν μπορεί ποτέ να αντιστοιχιστεί: έχει έναν κύκλο με δεδομένα ισχύος αλλά χωρίς αξιοποιήσιμη διάρκεια, επομένως το WashData δεν έχει καμία διάρκεια αναφοράς πάνω στην οποία να στηρίξει μια αντιστοίχιση. Καταγράψτε ξανά αυτό το πρόγραμμα ή ορίστε την αναμενόμενη διάρκειά του, για να επανέλθει στην αντιστοίχιση.", "pg_no_alerts": "Δεν εντοπίστηκαν προβλήματα σε αυτή την εκτέλεση.", "pg_override_active": "Χρησιμοποιούνται οι επεξεργασμένες ρυθμίσεις σας έναντι των τρεχουσών", - "pg_history_intro": "Αναπαράγετε τους πρόσφατους κύκλους σας μέσω του πραγματικού ανιχνευτή και αντιστοιχιστή. Επεξεργαστείτε πρώτα μια ρύθμιση ανίχνευσης στη λειτουργία Προσομοίωση για να δείτε εδώ μια σύγκριση πριν/μετά.", "pg_history_empty": "Πατήστε Εκτέλεση για να αναπαράγετε τους κύκλους σας.", "pg_history_summary": "{detected}/{total} ανιχνεύθηκαν · {correct} αντιστοιχίστηκαν σωστά", "pg_sweep_no_metric": "Δεν υπάρχουν αρκετά δεδομένα για τη βαθμολόγηση αυτού του στόχου.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Η σάρωση απέτυχε.", "hist_scan_expired": "Αυτή η σάρωση δεν είναι πλέον διαθέσιμη. Κάντε ξανά σάρωση.", "hist_import_failed": "Η εισαγωγή απέτυχε.", - "imported_history_readonly": "Ανιχνεύθηκε σε εισαγόμενο ιστορικό ισχύος. Διαμορφώνει την αντιστοίχιση προγραμμάτων, αλλά δεν προσμετράται στα στατιστικά σας και δεν μπορεί να περικοπεί ή να διαχωριστεί. Επισημάνετέ τον για να ορίσετε το πρόγραμμα." + "imported_history_readonly": "Ανιχνεύθηκε σε εισαγόμενο ιστορικό ισχύος. Διαμορφώνει την αντιστοίχιση προγραμμάτων, αλλά δεν προσμετράται στα στατιστικά σας και δεν μπορεί να περικοπεί ή να διαχωριστεί. Επισημάνετέ τον για να ορίσετε το πρόγραμμα.", + "odometer_intro": "Αυτό το σύνολο μόνο αυξάνεται. Η διαγραφή της εγγραφής ενός κύκλου δεν το αλλάζει και συνεχίζει να μετρά πέρα από το όριο του αποθηκευμένου ιστορικού, ώστε οι υπενθυμίσεις σέρβις να παραμένουν σωστές.", + "odometer_correct_hint": "Ορίστε αυτή την τιμή αν το WashData κατέγραψε μια λειτουργία που δεν έγινε ποτέ, ή αν η συσκευή είχε εκτελέσει κύκλους πριν από την εγκατάσταση του WashData.", + "no_reminders_set": "Δεν έχουν οριστεί ακόμη υπενθυμίσεις σέρβις.", + "toast_program_armed": "Πρόγραμμα ορίστηκε για τον επόμενο κύκλο: {program}" }, "phase_desc": { "anti_crease": "Περιστασιακά σύντομες ανατροπές μετά την ολοκλήρωση για μείωση των ρυτίδων.", @@ -1178,7 +1202,7 @@ "label": "Μέγιστη Ισχύς Κατά Τσακίσματος" }, "auto_label_confidence": { - "doc": "Εάν το σκορ του αγώνα στο τέλος του κύκλου είναι στο ή πάνω από αυτό, το πρόγραμμα επισημαίνεται αυτόματα χωρίς καμία προτροπή επιβεβαίωσης. Αυξήστε το ώστε να απαιτείται υψηλότερη βεβαιότητα πριν από την αυτόματη επισήμανση. χαμηλώστε το για να αυτοματοποιήσετε περισσότερο. Λειτουργεί σε συνδυασμό με το Learning Confidence κάτω από αυτό.", + "doc": "Εάν η βαθμολογία αντιστοίχισης στο τέλος του κύκλου είναι ίση ή μεγαλύτερη από αυτήν, το πρόγραμμα επισημαίνεται αυτόματα χωρίς καμία προτροπή επιβεβαίωσης. Αυξήστε την για να απαιτείται μεγαλύτερη βεβαιότητα πριν από την αυτόματη επισήμανση, μειώστε την για περισσότερη αυτοματοποίηση. Λειτουργεί σε συνδυασμό με την Εμπιστοσύνη Μάθησης ακριβώς από κάτω.", "label": "Εμπιστοσύνη Αυτόματης Επισήμανσης" }, "auto_maintenance": { @@ -1226,7 +1250,7 @@ "label": "Πλήθος Επαναλήψεων Λήξης" }, "energy_price_entity": { - "doc": "Αισθητήρας με την τρέχουσα τιμή ηλεκτρικής ενέργειας ανά kWh (π.χ. ένα δυναμικό τιμολόγιο). Προηγείται της στατικής τιμής παρακάτω. Κάθε κύκλος παγώνει την τιμή που ισχύει όταν τελειώσει.", + "doc": "Αισθητήρας με την τρέχουσα τιμή ηλεκτρικής ενέργειας ανά kWh (π.χ. ένα δυναμικό τιμολόγιο). Πρέπει να είναι τιμή, όχι η οντότητα ισχύος ή ενέργειας της ίδιας της πρίζας: προηγείται της στατικής τιμής παρακάτω, οπότε ένας μετρητής kWh εδώ κοστολογεί κάθε κύκλο με την ένδειξη του μετρητή αντί για το τιμολόγιό σας. Με ενεργό το Χρονικά Σταθμισμένο Κόστος, κάθε κύκλος χρεώνεται με την τιμή που ίσχυε σε κάθε στιγμή της λειτουργίας του· διαφορετικά παγιώνεται πάνω του η τιμή που ίσχυε όταν τελείωσε.", "label": "Οντότητα Τιμής Ενέργειας" }, "energy_price_static": { @@ -1254,7 +1278,7 @@ "label": "Αντιστροφή Εξωτερικού Ενεργοποιητή (ενεργοποίηση στο ΑΠΕΝΕΡΓΟ)" }, "learning_confidence": { - "doc": "Εάν το σκορ αντιστοίχισης πέσει μεταξύ αυτού και του Auto-Label Confidence, μια ειδοποίηση σχολίων σας ζητά να επαληθεύσετε το αναγνωρισμένο πρόγραμμα. Κάτω από αυτό το σκορ ο αγώνας είναι πολύ αβέβαιος για να βγει στην επιφάνεια. Πρέπει να διατηρείται κάτω από την αυτοπεποίθηση ετικέτας.", + "doc": "Εάν η βαθμολογία αντιστοίχισης βρίσκεται ανάμεσα σε αυτήν και την Εμπιστοσύνη Αυτόματης Επισήμανσης, μια ειδοποίηση ανατροφοδότησης σάς ζητά να επαληθεύσετε το πρόγραμμα που αναγνωρίστηκε. Κάτω από αυτήν τη βαθμολογία η αντιστοίχιση είναι πολύ αβέβαιη για να εμφανιστεί. Πρέπει να διατηρείται κάτω από την Εμπιστοσύνη Αυτόματης Επισήμανσης.", "label": "Εμπιστοσύνη Μάθησης" }, "linked_device": { @@ -1322,7 +1346,7 @@ "label": "Ενεργοποίηση Συμβάντων HA για Ειδοποιήσεις" }, "notify_icon": { - "doc": "Προαιρετικό εικονίδιο mdi για την ειδοποίηση (π.χ. mdi:πλυντήριο). Αφήστε κενό για την προεπιλογή της πλατφόρμας.", + "doc": "Το μικρό εικονίδιο που εμφανίζεται στην ειδοποίηση (π.χ. mdi:washing-machine). Το Android το σχεδιάζει στη γραμμή κατάστασης· το iOS το εμφανίζει στη θέση του εικονιδίου της εφαρμογής και απαιτεί τη συνοδευτική εφαρμογή 2026.8 ή νεότερη. Αφήστε κενό για την προεπιλογή της πλατφόρμας.", "label": "Εικονίδιο Ειδοποίησης" }, "notify_live_chronometer": { @@ -1422,7 +1446,7 @@ "label": "Κατώφλι Αντιστοίχισης" }, "profile_unmatch_threshold": { - "doc": "Εάν ένας ζωντανός αγώνας στα μέσα του κύκλου πέσει κάτω από αυτό το σκορ, η δοκιμαστική αναγνώριση διαγράφεται. Διατηρήστε το λίγο κάτω από το όριο αντιστοίχισης, ώστε μια σύντομη πτώση στην ομοιότητα να μην επαναφέρει την οθόνη σε απαράμιλλη.", + "doc": "Εάν μια ζωντανή αντιστοίχιση στη μέση του κύκλου πέσει κάτω από αυτήν τη βαθμολογία, η προσωρινή αναγνώριση διαγράφεται. Διατηρήστε την λίγο κάτω από το Κατώφλι Αντιστοίχισης, ώστε μια σύντομη πτώση της ομοιότητας να μην επαναφέρει την ένδειξη σε Χωρίς αντιστοίχιση.", "label": "Κατώφλι Μη-Αντιστοίχισης" }, "progress_reset_delay": { @@ -1433,10 +1457,6 @@ "doc": "Δευτερόλεπτα μια αντλία μπορεί να λειτουργεί συνεχώς πριν επισημανθεί ως πιθανώς κολλημένη (πυρίζει το συμβάν κολλημένης αντλίας).", "label": "Διάρκεια Κολλημένης Αντλίας" }, - "running_dead_zone": { - "doc": "Μετά την έναρξη ενός κύκλου, οι μειώσεις ισχύος σε αυτό το παράθυρο αγνοούνται. Τα πλυντήρια γεμίζουν με κρύο νερό (πέφτει κοντά στα 0 W πριν από τη θέρμανση) - χωρίς αυτή την προστασία, η φάση πλήρωσης μοιάζει με ένα τέλος κύκλου. Αυτό ΔΕΝ παρακάμπτει δεδομένα: το πλήρες ίχνος ισχύος καταγράφεται από το T=0. Η μηχανή προτάσεων μετρά το πραγματικό μοτίβο εκκίνησης του μηχανήματός σας και το μετράει αυτόματα.", - "label": "Νεκρή Ζώνη Λειτουργίας" - }, "sampling_interval": { "doc": "Αναμενόμενος χρόνος μεταξύ των μετρήσεων του αισθητήρα - χρησιμοποιείται για το μέγεθος του παραθύρου εξομάλυνσης και τη σωστή έναρξη της αναπήδησης. Κάθε ενημέρωση αισθητήρα καταγράφεται ανεξάρτητα από αυτήν την τιμή. βαθμονομεί μόνο τους κατάντη υπολογισμούς. Η μηχανή προτάσεων μετρά τον πραγματικό ρυθμό του αισθητήρα σας από προηγούμενους κύκλους και το ρυθμίζει αυτόματα.", "label": "Διάστημα Δειγματοληψίας" @@ -1534,7 +1554,7 @@ }, "keep_min_score": { "label": "Ελάχ. Βαθμός Αντιστοίχισης", - "doc": "Ελάχιστη βαθμολογία ομοιότητας για παραμονή υποψηφίου στον αγώνα αντιστοίχισης. Η προεπιλογή 0.1 είναι εκ προθέσεως επιεικής - επιτρέπει ακόμη και αδύναμους υποψηφίους, αφήνοντας τα επόμενα στάδια να βρουν το καλύτερο ταίριασμα. Αυξήστε για πρώιμο αποκλεισμό απίθανων προφίλ; μειώστε για διεύρυνση της αρχικής ομάδας υποψηφίων." + "doc": "Ελάχιστη βαθμολογία ομοιότητας που χρειάζεται ένας υποψήφιος για να παραμείνει στη διεκδίκηση της αντιστοίχισης. Η προεπιλογή 0.1 είναι εκ προθέσεως επιεικής - επιτρέπει ακόμη και αδύναμους υποψηφίους, αφήνοντας τα επόμενα στάδια να βρουν την καλύτερη αντιστοίχιση. Αυξήστε την για να αποκλείονται νωρίτερα τα απίθανα προφίλ, μειώστε την για να διευρύνετε την αρχική ομάδα υποψηφίων." }, "dtw_blend": { "label": "Ανάμειξη DTW", @@ -1564,13 +1584,17 @@ "label": "Διάστημα προφίλ ισχύος", "doc": "Το μέγεθος κουβά για το χαρακτηριστικό αισθητήρα power_profile ανά προφίλ (ο επίπεδος πίνακας μέσων βατ ανά θέση που καταναλώνουν εξωτερικοί σχεδιαστές όπως το EMHASS και το tibber_prices). Μικρότεροι κουβάδες διατηρούν τις σύντομες κορυφές ισχύος έντονες· μεγαλύτεροι κουβάδες εξομαλύνουν το σχήμα. Προεπιλογή 15 λεπτά. Μόνο κατά την ανάγνωση· δεν επηρεάζει την ανίχνευση." }, + "notify_live_silent": { + "label": "Αθόρυβες Ζωντανές Ενημερώσεις", + "doc": "Μόνο iOS. Ανανεώνει αθόρυβα τη ζωντανή πρόοδο: κάθε ενημέρωση φτάνει χωρίς ήχο ή δόνηση, με χαμηλότερη προτεραιότητα που εξοικονομεί μπαταρία και ενδέχεται να ομαδοποιηθεί για λίγο από το iOS. Η πρώτη ενημέρωση κάθε κύκλου (αυτή που ξεκινά το Live Activity) και οι ειδοποιήσεις έναρξης/ολοκλήρωσης εξακολουθούν να ειδοποιούν κανονικά. Απενεργοποιήστε το για να ειδοποιείστε σε κάθε ενημέρωση." + }, "notify_live_sticky": { "label": "Διατήρηση ζωντανής ειδοποίησης μετά το πάτημα", "doc": "Μόνο Android. Κάντε την ειδοποίηση ζωντανής προόδου μόνιμη (sticky) ώστε το πάτημά της να μη σβήνει το ενεργό νήμα. Απενεργοποιημένο διατηρεί την προεπιλεγμένη συμπεριφορά όπου το πάτημα σβήνει την ειδοποίηση." }, "notify_live_click_action": { - "label": "Στόχος πατήματος ζωντανής ειδοποίησης", - "doc": "Μόνο Android. Πού ανοίγει το πάτημα της ειδοποίησης ζωντανής προόδου (π.χ. /lovelace/laundry ή πλήρες URL) αντί για την αρχική σελίδα της εφαρμογής. Αφήστε κενό για την προεπιλογή." + "label": "Στόχος πατήματος ειδοποίησης", + "doc": "Πού οδηγεί το πάτημα μιας ειδοποίησης από αυτή τη συσκευή, σε Android και iOS. Αν μείνει κενό, ανοίγει ο πίνακας WashData σε αυτή τη συσκευή. Δώστε μια διαδρομή πίνακα ελέγχου (π.χ. /lovelace/laundry) ή ένα πλήρες URL για να το στείλετε αλλού, ή \"none\" για να μην υπάρχει καθόλου στόχος πατήματος." }, "door_opens_at_end": { "label": "Η πόρτα ανοίγει αυτόματα στο τέλος", @@ -1587,6 +1611,18 @@ "profile_evidence_sources": { "label": "Κύκλοι που διαμορφώνουν ένα πρόγραμμα", "doc": "Ποιοι κύκλοι χρησιμοποιούνται για τη δημιουργία της καμπύλης ισχύος κάθε προγράμματος και για την αντιστοίχιση ενός ολοκληρωμένου κύκλου με αυτήν. Αν αφαιρέσετε την επιλογή ενός είδους, αυτό σταματά να διαμορφώνει τα προγράμματά σας χωρίς να διαγράφεται τίποτα - οι κύκλοι παραμένουν στη λίστα Κύκλοι και μπορείτε ακόμη να τους επισημάνετε ή να τους αφαιρέσετε. Χρήσιμο αν δεν εμπιστεύεστε τα εισαγόμενα δεδομένα. Τα στατιστικά δεν επηρεάζονται: μετρούν πάντα μόνο τους κύκλους που εκτέλεσε πραγματικά αυτό το μηχάνημα. Η αφαίρεση όλων των επιλογών αγνοείται, καθώς ένα πρόγραμμα χωρίς κύκλους πίσω του δεν θα μπορούσε ποτέ να αντιστοιχιστεί." + }, + "energy_price_dynamic": { + "label": "Χρονικά Σταθμισμένο Κόστος", + "doc": "Χρεώνει κάθε κύκλο με την τιμή που ίσχυε σε κάθε στιγμή της λειτουργίας του, αντί για τη μία τιμή που ίσχυε όταν τελείωσε. Ισχύει μόνο με οντότητα τιμής (μια στατική τιμή δεν μεταβάλλεται). Δεν χρειάζεται επιπλέον ρύθμιση: η τιμή παρακολουθείται ζωντανά όσο τρέχει ο κύκλος, ενώ η Επεξεργασία ιστορικού στα Διαγνωστικά επανυπολογίζει το κόστος παλαιότερων κύκλων από τον recorder. Ανενεργό = η κλασική συμπεριφορά, μία τιμή παγιωμένη στο τέλος." + }, + "anti_crease_finalize_ratio": { + "label": "Αναλογία Οριστικοποίησης Κατά Τσακίσματος", + "doc": "Πόσο μέσα στην αναμενόμενη διάρκεια του αντιστοιχισμένου προγράμματος πρέπει να έχει προχωρήσει ένας κύκλος προτού το WashData μπορέσει να κρίνει ότι η περιστροφή χαμηλής ισχύος που βλέπει είναι η ουρά κατά τσακίσματος και να τερματίσει εκεί τον κύκλο. Η αναμενόμενη διάρκεια είναι ο μέσος όρος του προγράμματος, οπότε σε ένα στεγνωτήριο του οποίου ο χρόνος στεγνώματος με αισθητήρα ακολουθεί το φορτίο, οι περισσότερες εκτελέσεις δεν φτάνουν ποτέ στο 98% της και η ουρά παραμένει μέχρι το εφεδρικό χρονικό όριο, συχνά 15 έως 40 λεπτά αφότου σταμάτησε πραγματικά το μηχάνημα. Σε τέτοιο στεγνωτήριο χαμηλώστε την τιμή (π.χ. 0,75). Αφήστε τα πλυντήρια ρούχων στην προεπιλογή: ένα πλυντήριο περνά το μεγαλύτερο μέρος του κύκλου του κάτω από τη Μέγιστη Ισχύ Κατά Τσακίσματος, άρα αυτό το κλάσμα είναι το μόνο που ξεχωρίζει μια ήσυχη ενδιάμεση φάση πλύσης από την ουρά, και αν το χαμηλώσετε μπορεί να κοπεί μια πλύση στη μέση. Αφήστε το κενό για την προεπιλογή (0,98). Πρόκειται για διαφορετική ρύθμιση από την Αναλογία Έξυπνου Τερματισμού, η οποία ελέγχει έναν ξεχωριστό έλεγχο πρόωρης ολοκλήρωσης." + }, + "curve_preroll_seconds": { + "label": "Προεγγραφή Καμπύλης", + "doc": "Πόσα δευτερόλεπτα μετρήσεων από ματαιωμένες απόπειρες εκκίνησης μπορεί να μεταφέρει το WashData στην αρχή της καμπύλης ενός κύκλου. Η καμπύλη ενός κύκλου ξεκινά κανονικά από την απόπειρα εκκίνησης που τελικά πετυχαίνει, οπότε σε ένα μηχάνημα που δοκιμάζει μερικές φορές πριν σταθεροποιηθεί (επιλογή προγράμματος, κλείδωμα πόρτας, πρώτο γέμισμα) τα πρώτα λεπτά της πραγματικής δραστηριότητας λείπουν από κάθε καμπύλη. Ορίστε την τιμή λίγο πάνω από τη μεγαλύτερη τέτοια περίοδο προετοιμασίας που έχει το μηχάνημά σας (τα 300 δευτ. αρκούν για τα περισσότερα). Το 0 την απενεργοποιεί. Προσοχή: η ενεργοποίηση μετατοπίζει νωρίτερα την καταγεγραμμένη έναρξη, οπότε οι κύκλοι που καταγράφηκαν πριν και μετά την αλλαγή έχουν διαφορετικές διάρκειες για το ίδιο πρόγραμμα. Οι μέσοι όροι που έχουν μαθευτεί θα αποκλίνουν μέχρι να φύγουν οι παλαιότεροι κύκλοι." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Διαίρεση…", "deleting": "Διαγραφή…", "imported": "Εισηγμένο", - "preparing": "Προετοιμασία…" + "preparing": "Προετοιμασία…", + "unlabelling": "Αφαίρεση επισήμανσης…" }, "tab": { "advanced": "Για προχωρημένους", @@ -1778,6 +1815,7 @@ "recording_stopped": "Η εγγραφή σταμάτησε", "review_saved": "Η κριτική αποθηκεύτηκε", "settings_conflicts": "Επιλύστε τις επισημασμένες συγκρούσεις ρυθμίσεων πριν αποθηκεύσετε.", + "settings_no_changes": "Δεν υπάρχουν αλλαγές για αποθήκευση", "settings_reverted": "Οι ρυθμίσεις επαναφέρθηκαν. επαναφόρτωση ενσωμάτωσης", "settings_saved": "Οι ρυθμίσεις αποθηκεύτηκαν. επαναφόρτωση ενσωμάτωσης", "suggestions_applied": "Εφαρμόστηκαν προτάσεις. επαναφόρτωση ενσωμάτωσης", @@ -1836,7 +1874,11 @@ "export_selective_done": "Η εξαγωγή λήφθηκε", "import_selective_done": "Εισήχθησαν {profiles} προφίλ και {cycles} κύκλοι", "hist_csv_required": "Φορτώστε πρώτα ένα αρχείο CSV ή επικολλήστε το περιεχόμενό του", - "file_read_failed": "Δεν ήταν δυνατή η ανάγνωση του αρχείου" + "file_read_failed": "Δεν ήταν δυνατή η ανάγνωση του αρχείου", + "unlabel_done": "Αφαιρέθηκε η επισήμανση από {count} κύκλους", + "unlabel_failed": "Η αφαίρεση επισήμανσης απέτυχε: {error}", + "odometer_saved": "Το σύνολο κύκλων αποθηκεύτηκε", + "odometer_save_failed": "Δεν ήταν δυνατή η αποθήκευση του συνόλου: {error}" }, "suggestion": { "both_agree": "Το WashData προτείνει", @@ -1883,10 +1925,6 @@ "higher": "Επιβεβαιώνει μόνο αντιστοιχίσεις υψηλής εμπιστοσύνης; περισσότεροι κύκλοι παραμένουν χωρίς ετικέτα", "lower": "Αντιστοιχεί περισσότερους κύκλους; κάποιοι μπορεί να είναι ελαφρώς λανθασμένοι" }, - "running_dead_zone": { - "higher": "Αγνοεί σύντομες αιχμές ισχύος κατά την αδράνεια – λιγότερο ευαίσθητο σε θόρυβο", - "lower": "Ανταποκρίνεται σε συντομότερες αναλαμπές ισχύος – εντοπίζει γρήγορη παροδική δραστηριότητα" - }, "start_threshold_w": { "higher": "Αποφεύγει ψευδείς εκκινήσεις από σύντομες αιχμές ισχύος", "lower": "Εντοπίζει προγράμματα χαμηλής ισχύος; μπορεί να ενεργοποιηθεί από θόρυβο" @@ -1932,7 +1970,9 @@ "thr_batch": "Διατηρείται λίγο πάνω από τη χαμηλότερη ενεργό ισχύ p05 σε {cycles} κύκλους ({p05}W) ώστε μια έναρξη να εντοπίζεται όσο το δυνατόν νωρίτερα και το κατώφλι διακοπής να παραμένει κάτω από τη χαμηλότερη ισχύ λειτουργίας της μηχανής.", "tol_per_profile": "p75 της διακύμανσης διάρκειας ανά προφίλ σε {profiles} προφίλ ({cycles} κύκλοι)· τα σταθερά προφίλ δεν επιβαρύνονται.", "tol_pooled": "Βάσει της συγκεντρωτικής διακύμανσης διάρκειας {cycles} πρόσφατων κύκλων με ετικέτα (απόκλιση p95={dev}).", - "watchdog": "Διατηρείται όσο το δυνατόν χαμηλότερο με ασφάλεια (λίγο πάνω από το κενό ενημέρωσης p95 των {p95}s και τουλάχιστον 2× το διάστημα δειγματοληψίας των {median}s, ελάχ. 30s) ώστε οι διακοπές να εντοπίζονται γρήγορα χωρίς ψευδείς διακοπές." + "watchdog": "Διατηρείται όσο το δυνατόν χαμηλότερο με ασφάλεια (λίγο πάνω από το κενό ενημέρωσης p95 των {p95}s και τουλάχιστον 2× το διάστημα δειγματοληψίας των {median}s, ελάχ. 30s) ώστε οι διακοπές να εντοπίζονται γρήγορα χωρίς ψευδείς διακοπές.", + "match_interval_capped": "Περιορίστηκε ώστε {persistence} διαδοχικές αντιστοιχίσεις να χωρούν στο {pct}% του συντομότερου προγράμματος ({shortest}s)· μόνο ο ρυθμός ενημέρωσης (διάμεσος={median}s) θα πρότεινε μεγαλύτερο διάστημα.", + "match_interval_floored": "Μόνο το συντομότερο πρόγραμμα ({shortest}s) θα περιόριζε αυτή την τιμή στα {cap}s, κάτω από το ελάχιστο των {minimum}s, οπότε παραμένει στο ελάχιστο: {persistence} διαδοχικές αντιστοιχίσεις χρειάζονται {budget}s, δηλαδή περισσότερο από το {pct}% αυτού του προγράμματος. Η αντιστοίχιση εκτελείται μόνο όταν φτάνει μια νέα μέτρηση (διάμεσος={median}s), επομένως το ελάχιστο δεν κοστίζει τίποτα." }, "exclusions": { "summary": "Εξαιρέθηκαν {total} εσφαλμένα εντοπισμένοι κύκλοι: {parts}.", @@ -1982,7 +2022,7 @@ "energy_weight": "Στάδιο 4: πόσο επηρεάζει η συμφωνία ενέργειας τον τελικό βαθμό (προεπιλογή 0.22)", "profile_match_min_duration_ratio": "Στάδιο 1: ελάχιστο μήκος κύκλου ως κλάσμα της διάρκειας προφίλ; προεπιλογή 0.1 (10%)", "profile_match_max_duration_ratio": "Στάδιο 1: μέγιστο μήκος κύκλου ως κλάσμα της διάρκειας προφίλ; προεπιλογή 1.5 (150%)", - "keep_min_score": "Στάδιο 2: κατώτατο όριο βαθμολογίας για παραμονή στον αγώνα; προεπιλογή 0.1 επιτρέπει αδύναμα ταιριάσματα, τα επόμενα στάδια επιλέγουν το καλύτερο", + "keep_min_score": "Στάδιο 2: κατώτατη βαθμολογία για παραμονή στη διεκδίκηση, η προεπιλογή 0.1 επιτρέπει αδύναμες αντιστοιχίσεις και τα επόμενα στάδια επιλέγουν την καλύτερη", "dtw_blend": "Στάδιο 3: 0 = μόνο βαθμός πυρήνα, 1 = μόνο βαθμός DTW, 0.5 = ισόρροπη ανάμειξη (προεπιλογή)", "dtw_ensemble_w": "Στάδιο 3 (λειτουργία συνόλου): βάρος στο κλιμακωμένο-L1 έναντι παραγώγου DTW; προεπιλογή 0.7 ευνοεί το επίπεδο ισχύος", "dtw_ddtw_scale": "Στάδιο 3: απόσταση ημίσεος κορεσμού DDTW; μικρότερη = μεγαλύτερη ευαισθησία σχήματος (προεπιλογή 30)", @@ -1990,7 +2030,9 @@ "duration_scale": "Στάδιο 4: λόγος λογαρίθμου όπου η συμφωνία διάρκειας υποδιπλασιάζεται; μικρότερος = αυστηρότερη ποινή (προεπιλογή 0.175)", "energy_scale": "Στάδιο 4: λόγος λογαρίθμου όπου η συμφωνία ενέργειας υποδιπλασιάζεται; μικρότερος = αυστηρότερη ποινή (προεπιλογή 0.25)", "dishwasher_end_spike_quiet_release": "Πλυντήριο πιάτων: δευτερόλεπτα ησυχίας μετά την αναμενόμενη διάρκεια πριν απελευθερωθεί η αναμονή αποστράγγισης τέλους κύκλου", - "smart_termination_duration_ratio": "Κλάσμα της αναμενόμενης διάρκειας του αντιστοιχισμένου προγράμματος που πρέπει να φτάσει ένας κύκλος προτού ο Έξυπνος Τερματισμός μπορέσει να τον τερματίσει νωρίς· χαμηλώστε το για μηχανήματα που εξαρτώνται από το φορτίο ή τη θερμοκρασία" + "smart_termination_duration_ratio": "Κλάσμα της αναμενόμενης διάρκειας του αντιστοιχισμένου προγράμματος που πρέπει να φτάσει ένας κύκλος προτού ο Έξυπνος Τερματισμός μπορέσει να τον τερματίσει νωρίς· χαμηλώστε το για μηχανήματα που εξαρτώνται από το φορτίο ή τη θερμοκρασία", + "anti_crease_finalize_ratio": "Κλάσμα της αναμενόμενης διάρκειας του αντιστοιχισμένου προγράμματος που πρέπει να φτάσει ένας κύκλος προτού οριστικοποιηθεί η ουρά περιστροφής κατά τσακίσματος· χαμηλώστε το σε στεγνωτήρια με μεγάλη ουρά, αφήστε τα πλυντήρια στην προεπιλογή", + "curve_preroll_seconds": "Δευτερόλεπτα μετρήσεων από ματαιωμένες απόπειρες εκκίνησης που μεταφέρονται στην αρχή της καμπύλης ενός κύκλου· 0 = ανενεργό" }, "col": { "profile_tip": "Όνομα αντιστοιχισμένου προγράμματος. Χωρίς ετικέτα σημαίνει ότι κανένα προφίλ δεν αντιστοιχίστηκε στο τέλος του κύκλου.", @@ -1998,10 +2040,11 @@ "date_tip": "Ημερομηνία και ώρα έναρξης του κύκλου.", "duration_tip": "Συνολικός χρόνος λειτουργίας του κύκλου από την αρχή έως το τέλος.", "energy_tip": "Συνολική ενέργεια που καταναλώθηκε (kWh). Υπολογίζεται με ολοκλήρωση της ισχύος στον χρόνο.", - "cost_tip": "Κόστος ενέργειας για αυτόν τον κύκλο, παγιωμένο κατά την ολοκλήρωση με βάση την ισχύουσα τότε τιμή (ενέργεια x τιμή ανά kWh). Ορίστε μια τιμή στις Ρυθμίσεις για να συμπληρωθεί.", + "cost_tip": "Κόστος ενέργειας για αυτόν τον κύκλο, παγιωμένο κατά την ολοκλήρωση. Με ενεργό το Χρονικά Σταθμισμένο Κόστος, η καμπύλη ισχύος χρεώνεται με την τιμή που ίσχυε σε κάθε στιγμή· διαφορετικά ενέργεια x η μία τιμή που ίσχυε στο τέλος. Ορίστε μια τιμή στις Ρυθμίσεις για να συμπληρωθεί.", "confidence_tip": "Εμπιστοσύνη αντιστοίχισης προφίλ (0-100%). Πόσο στενά η καμπύλη ισχύος του κύκλου ταίριαξε με το αναγνωρισμένο πρόγραμμα.", "health_tip": "Υγεία κύκλου ML (υψηλότερο = καλύτερο). Κάντε κλικ σε έναν κύκλο για να τον ελέγξετε και να τον αξιολογήσετε.", - "flags_tip": "Σημαίες αναθεώρησης, ανωμαλίας και πηγής για τον κύκλο. Τοποθετήστε τον δείκτη πάνω σε ένα εικονίδιο για λεπτομέρειες." + "flags_tip": "Σημαίες αναθεώρησης, ανωμαλίας και πηγής για τον κύκλο. Τοποθετήστε τον δείκτη πάνω σε ένα εικονίδιο για λεπτομέρειες.", + "cost_dynamic_tip": "Χρονικά σταθμισμένο: χρεώθηκε με την τιμή που ίσχυε κατά τη διάρκεια του κύκλου (πραγματική τιμή {price} {cur}/kWh)." }, "stat": { "avg": "μ.ό.", @@ -2028,9 +2071,9 @@ "finished": "Ο κύκλος έφτασε σε τελική κατάσταση και ολοκληρώθηκε." }, "store": { + "search_ph2": "Αναζήτηση με μάρκα ή μοντέλο…", "your_model_tip": "Αυτή είναι η συσκευή που δηλώσατε στις Ρυθμίσεις", "your_model": "Δική σας", - "search_brand_ph": "Αναζήτηση κατά μάρκα…", "programs_count": "Προγράμματα: {n}", "browse": "Περιήγηση", "device": "Συσκευή", @@ -2075,7 +2118,8 @@ "golden": "Επανεπεξεργασία: επισήμανση κύκλων αναφοράς", "suggestions": "Επανεπεξεργασία: προτάσεις", "ml_training": "Επανεπεξεργασία: εκπαίδευση ML", - "health": "Επανεπεξεργασία: υγεία κύκλων" + "health": "Επανεπεξεργασία: υγεία κύκλων", + "costs": "Επανεπεξεργασία: κόστος ενέργειας" }, "pg_sweep": { "optimize": "Βελτιστοποίηση: {param}" diff --git a/custom_components/ha_washdata/translations/panel/en.json b/custom_components/ha_washdata/translations/panel/en.json index 10db94fb..6fa5dd19 100644 --- a/custom_components/ha_washdata/translations/panel/en.json +++ b/custom_components/ha_washdata/translations/panel/en.json @@ -65,6 +65,9 @@ "feedback_requested": "Feedback requested", "golden_cycle": "Recorded reference cycle", "imported_tip": "Imported from the community store. Used for matching only, not counted in stats.", + "unmatchable": "can't be matched", + "quiet_tail": "~{mins}m quiet tail", + "quiet_tail_tip": "Near the end this program goes quiet for about {mins} min, then draws about {watts} W for {secs} s before finishing. Seen in {seen} of {measured} measured cycles: the appliance does not do it every run.", "improving": "↗ improving", "manual": "(manually selected)", "needs_review": "Needs review", @@ -79,7 +82,8 @@ "underrun": "Finished faster than usual", "underrun_ratio": "({pct}% of expected)", "using_defaults": "Using shipped defaults", - "using_tuned": "Using tuned weights" + "using_tuned": "Using tuned weights", + "armed": "(applies to the next cycle)" }, "btn": { "add_device": "+ Add device", @@ -161,6 +165,7 @@ "on_cycle_started": "On cycle started", "pause_cycle": "Pause", "pause_cycle_tip": "Pause the running cycle – the appliance will resume where it left off", + "pg_apply_best": "Apply best", "pg_apply_device": "Apply to device", "pg_apply_to_settings": "Save to settings", "pg_apply_to_settings_tip": "Copy the values you edited here into this device's live settings", @@ -201,6 +206,7 @@ "reset": "Reset", "reset_ml_models_tip": "Discard the fine-tuned models and go back to the built-in ones. WashData can re-learn them later.", "reset_muted": "Reset muted", + "reset_zoom": "Reset zoom", "reset_to_builtin": "Reset to built-in models", "reset_to_defaults": "Reset to defaults", "resume_cycle": "Resume cycle", @@ -227,7 +233,6 @@ "save_reminders": "Save reminders", "save_review": "Save Review", "save_settings": "Save Settings", - "search": "Search", "select": "Select", "select_all": "Select all", "select_last20": "Last 20", @@ -247,8 +252,11 @@ "trim": "Trim", "trim_split": "Trim / Split", "undo": "Undo", + "unlabel_selected": "Unlabel selected ({n})", + "unlabel_selected_tip": "Remove the profile label from the selected cycles. They stay in your history and can be labelled again later - they just stop shaping this profile.", "use": "Use", - "wipe_all": "Wipe All Data" + "wipe_all": "Wipe All Data", + "save_total_cycles": "Save total" }, "conflict": { "anti_wrinkle_exit": { @@ -261,6 +269,12 @@ }, "attn_sub": "Fix conflicts before saving", "attn_title": "Setting conflicts: {n}", + "price_entity": { + "power_sensor": "This is the device's Power Sensor, not a price. A price entity overrides the static price below, so cycles would be costed at watts per kWh - clear this field.", + "energy_sensor": "This is the device's Energy Meter, not a price. A price entity overrides the static price below, so cycles would be costed at the meter reading per kWh - clear this field.", + "device_class": "This sensor measures {dc}, not a price per kWh. A price entity overrides the static price below - clear this field or point it at a tariff sensor.", + "unit": "This sensor reads in {unit}, which is not a price per kWh. A price entity overrides the static price below - clear this field or point it at a tariff sensor." + }, "settings_banner": "Setting conflicts: {n}. Check the highlighted sections and fix them before saving.", "settings_banner_btn": "Go to first", "confidence": { @@ -349,11 +363,14 @@ "panel_settings": "Panel Settings", "param_sweep": "Parameter sweep", "pg_across_cycles": "Across your cycles", + "pg_detection_params": "Detection settings", + "pg_outcome": "Simulation outcome", "pg_settings_source": "Settings source", "phase_catalog": "Phase Catalog", "playground": "Playground", "process_history": "Process History", "recommendations": "Recommendations ({n})", + "service_status": "Service Status", "settings_history": "Settings history", "status": "Status", "status_graph": "Status Graph", @@ -381,6 +398,7 @@ "aria_phase_chart": "Phase editor chart", "aria_playground_chart": "Cycle power trace with draggable detection thresholds", "aria_playground_chart2": "Interactive cycle power graph: hover to read time/power, scroll to zoom, drag to pan", + "aria_playground_chart3": "Interactive cycle power graph: hover to read time/power, scroll or pinch to zoom, drag to pan", "aria_power_chart": "Power consumption chart", "aria_spaghetti_chart": "Overlaid cycle power traces", "aria_sweep_chart": "Parameter sweep results chart", @@ -441,6 +459,7 @@ "days": "days", "debug_traces": "Debug Traces", "default": "Default", + "device_unresolved": "Unavailable device ({id})", "default_access_level": "Default level for users not listed below", "default_other": "Default (other devices)", "default_tab": "Default tab when opening the panel", @@ -535,23 +554,45 @@ "pct_match_confidence": "{pct}% match confidence", "peak_power_short": "Peak", "per_cycle_short": "cycle", + "pg_alert_ambiguous": "Ambiguous match", + "pg_alert_did_not_finish": "Did not finish", + "pg_alert_energy": "Energy anomaly", + "pg_alert_false_end": "Split into multiple cycles", "pg_alert_indefinite": "Would run indefinitely", + "pg_alert_overrun": "Overrun", "pg_alert_stress_cap": "Hit safety cap", "pg_alert_stress_ok": "Idle termination: cycle stopped", "pg_alert_stress_warn": "Idle draw above stop threshold", "pg_alert_timeout_end": "Ended by timeout, not prediction", + "pg_alert_underrun": "Underrun", + "pg_alert_unmatched": "Unmatched", "pg_anti_wrinkle": "Anti-wrinkle", "pg_autofill_tip": "Auto-fill range from current value ({lo}–{hi})", "pg_best_marker": "Best: {v}", "pg_best_value": "Best value found", + "pg_broke": "broke", "pg_current_value": "Current value:", "pg_detecting": "Detecting", "pg_drag_hint": "Drag line on graph", "pg_dtw": "DTW", + "pg_end_timing_changed": "end-timing changed", + "pg_ended": "Ended", "pg_envelope_fit": "Envelope fit", "pg_envelope_fit_tip": "How closely the cycle sits inside this profile's saved power envelope - a different lens than match confidence.", + "pg_ev_ambiguous": "Ambiguous", + "pg_ev_detected": "Detected", "pg_ev_ending": "Ending", + "pg_ev_finished": "Finished", + "pg_ev_match": "Match committed", + "pg_ev_match_changed": "Match changed", + "pg_ev_notify_finish": "Finish notification", + "pg_ev_notify_held": "Notification held (quiet hours)", + "pg_ev_notify_milestone": "Milestone notification", + "pg_ev_notify_pre": "Almost-done notification", + "pg_ev_notify_start": "Start notification", "pg_ev_running": "Running", + "pg_ev_unmatched": "Unmatched", + "pg_fixed": "fixed", "pg_group_advanced": "Edge cases", "pg_group_detection": "Detection triggers", "pg_group_matching": "Program matching", @@ -561,13 +602,24 @@ "pg_load_run": "Reload this run", "pg_match_confidence": "Match confidence", "pg_match_score": "Match score", + "pg_mode_history": "Test on history", "pg_mode_optimize": "Optimize", + "pg_newly_correct": "newly correct", + "pg_obj_ambiguity": "Ambiguity rate", + "pg_obj_endtiming": "End-timing accuracy", + "pg_obj_falseend": "False-end rate", + "pg_obj_match": "Match accuracy", + "pg_obj_overrun": "Duration off-target", + "pg_objective": "Objective", "pg_pct_of": "{p}% of {total}", "pg_poor_match": "Poor match", "pg_preset": "Playground preset", "pg_preset_name": "New preset name", "pg_preset_none": "Select a preset…", + "pg_proj_energy": "Proj. energy", "pg_recent_runs": "Recent runs", + "pg_regressed": "regressed", + "pg_steps": "Steps", "pg_stress_group": "Idle termination test", "pg_stress_idle_auto": "Auto", "pg_stress_idle_w": "Idle level (W)", @@ -575,6 +627,7 @@ "pg_stress_toggle": "Test idle termination", "pg_stress_toggle_desc": "Simulates the appliance staying at its standby draw after recording ends -- shows if and when WashData stops the cycle.", "pg_strong_match": "Strong match", + "pg_sweep_param": "Parameter", "pg_tile_ambiguous": "Ambiguous", "pg_tile_ambiguous_tip": "Cycles with a near-tie between two programs", "pg_tile_detected": "Detected", @@ -582,8 +635,12 @@ "pg_tile_matched": "Matched", "pg_tile_matched_tip": "Cycles correctly attributed to a known program", "pg_time_left": "Time left", + "pg_time_left_model": "Time left (model)", + "pg_time_left_model_tip": "Model-estimated time remaining (phase estimator + ML blend), not a static countdown", + "pg_vs_current": "vs current", "pg_weak_match": "Weak match", "pg_zoom_hint": "double-click to reset zoom", + "phase": "Phase", "phase_name": "Name", "phase_timeline": "Phase timeline", "power": "Power", @@ -591,8 +648,10 @@ "profile_index": "profile index →", "profile_name": "Profile Name", "program": "Program", - "program_tip": "Override which profile is matched to the current cycle. Auto-detect lets the integration pick the best match automatically. Pin a specific program to force-match it when auto-detect is wrong or you know what is running.", + "program_tip": "Override which profile is matched to the current cycle. Auto-detect lets the integration pick the best match automatically. Pin a specific program to force-match it when auto-detect is wrong or you know what is running. Pick one before starting the appliance and it is applied as soon as the next cycle begins.", "progress": "Progress", + "envelope_position": "curve {pct}%", + "envelope_position_tip": "Position on the matched program's own recorded curve, {pct}%. Measured by aligning this cycle against the profile instead of counting time, so it stays right when a run is longer or shorter than usual. It refreshes while the appliance is quiet, so it can lag behind during an active phase.", "quality": "Quality", "rate_n": "{n} stars", "rate_quality": "Rate quality", @@ -659,7 +718,13 @@ "unlabelled": "Unlabelled", "unlabelled_paren": "(unlabelled)", "warp_path": "Warp path", - "zoom_hint": "scroll to zoom · dblclick to reset" + "tap_to_dismiss": "tap the chart to dismiss", + "zoom_hint": "scroll to zoom · dblclick to reset", + "zoom_hint_touch": "pinch to zoom", + "zoom_hint_touch_zoomed": "pinch to zoom · double-tap to reset", + "cycles_since_service": "{since} / {total} cycles", + "total_cycles_run": "cycles run in total", + "correct_total_cycles": "Correct the total" }, "log": { "all_levels": "All levels", @@ -734,7 +799,7 @@ "merge_cycles": "Merge {n} Cycles", "new_group": "New profile group", "new_phase": "New Phase", - "process_history_msg": "Re-run matching, refresh suggestions, retrain ML (if enabled) and recompute cycle health across all stored cycles. This may take a while.", + "process_history_msg": "Re-run matching, refresh suggestions, retrain ML (if enabled), recost cycles against recorded energy prices and recompute cycle health across all stored cycles. This may take a while.", "process_history_ok": "Process", "process_history_title": "Process History", "process_recording": "Process Recording", @@ -757,6 +822,8 @@ "advisory_poor_health": "'{name}' has a low fit score - its recent cycles vary a lot or match weakly. Review its cycles or re-record the profile so matching and time estimates stay accurate.", "advisory_shape_drift": "'{name}' has drifted significantly from its original power shape. The appliance may have changed behaviour over time (e.g. limescale, wear). Consider re-recording this profile with recent cycles.", "advisory_shape_drift_corr": "'{name}' has drifted significantly from its original power shape (correlation {corr}). The appliance may have changed behaviour over time (e.g. limescale, wear). Consider re-recording this profile with recent cycles.", + "advisory_unmatchable": "'{name}' can never be matched: it has no cycle with power data behind it, so WashData has nothing to compare a running cycle against. Label a cycle of this program (or record one) to bring it back into matching.", + "advisory_unmatchable_no_duration": "'{name}' can never be matched: it has a cycle with power data but no usable duration, so WashData has no length to size a match against. Re-record this program, or set its expected duration, to bring it back into matching.", "analyzing": "Analyzing…", "appliance_monitor": "Appliance monitor", "artifact_dip_detail": "Dropped below the usual power band for ~{n}s.", @@ -766,7 +833,7 @@ "artifact_spike_detail": "Drew above the usual power band for ~{n}s.", "auto_label_intro": "Assign profiles to unlabelled cycles whose match confidence clears the threshold.", "automations_intro": "WashData fires {start} / {end} events and exposes entities, so notifications and actions are best built as normal Home Assistant automations. Automations that use this device appear below.", - "cleanup_intro": "Every labelled cycle overlaid. Tick outliers and delete to clean up the profile.", + "cleanup_intro": "Every labelled cycle overlaid. Tick outliers, then unlabel them (kept in history, but no longer shaping this profile) or delete them outright.", "clear_debug_hint": "Remove stored debug data to free space.", "collecting_data": "Collecting data. Cycles still needed before fine-tuning can start: {need} ({current}/{min}).", "compare_overlay_profiles": "Overlay profiles (faint)", @@ -869,10 +936,13 @@ "no_profiles_overlay": "No profiles to overlay.", "no_profiles_yet": "No profiles yet. Create one from a labelled cycle.", "no_profiles_yet_short": "No profiles yet.", + "no_reminders_set": "No service reminders are set yet.", "no_settings_match": "No settings match \"{q}\"", "no_split_points": "No split points yet.", "no_suggestions": "No active suggestions.", "notify_services_hint": "Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.", + "odometer_correct_hint": "Set this if WashData recorded a run that never happened, or if the appliance had already run cycles before WashData was installed.", + "odometer_intro": "This total only ever rises. Deleting a cycle record does not change it, and it keeps counting past the stored-history limit, so service reminders stay correct.", "old_actions_warning": "Configured with the old actions editor (now removed). They still fire on cycle events but can no longer be edited here. Convert them into a normal automation, or remove them.", "onboard_download": "New device? Adopt a ready-made setup (programs, reference cycles and phases) from another WashData user with the same appliance.", "onboarding_progress": "{n} / 3 cycles observed", @@ -891,14 +961,19 @@ "pg_apply_settings_confirm": "Save these {n} setting(s) to this device? {list}", "pg_canvas_empty": "Select a cycle above, then press Load to see its power trace.", "pg_canvas_empty2": "Pick a cycle above and press Run to simulate it. Then hover to read values, scroll to zoom, and drag to pan.", + "pg_canvas_empty3": "Pick a cycle above and press Run to simulate it. Then hover to read values, scroll or pinch to zoom, and drag to pan.", "pg_chart_caption": "Detection/match rate across values of {param}", "pg_ctrl_intro": "Values start from this device's live integration settings. Edits stay in the Playground until you publish them.", + "pg_history_empty": "Press Run to replay your cycles.", "pg_history_intro2": "Replay your recent cycles through the real detector and matcher with the settings above. Click any row to load that cycle in the graph; edit a setting to see a before/after comparison.", + "pg_history_summary": "{detected}/{total} detected · {correct} matched correctly", "pg_live_unavailable": "Live settings unavailable, showing defaults.", "pg_load_live_confirm": "Discard {n} Playground edit(s) and reload the integration's current settings?", "pg_match_rate": "{pct}% match rate on {total} cycles", "pg_matches_live": "Matches live settings", "pg_n_changed": "{n} changed vs live settings", + "pg_no_alerts": "No issues detected in this run.", + "pg_override_active": "Using your edited settings vs. current", "pg_preset_delete_confirm": "Delete the preset \"{name}\"?", "pg_preset_limit": "Preset limit reached ({n})", "pg_preset_overwrite": "Overwrite the preset \"{name}\"?", @@ -917,6 +992,8 @@ "pg_stress_terminated_detail": "Settled to ~{idle}W idle -- cycle ended {h}h {m}m later via {reason}.", "pg_sugg_none": "All suggestions already match the current settings", "pg_sweep_intro": "What if {param} were different? Test {steps} values across your last {cycles} cycles to find the setting where the most cycles are correctly matched.", + "pg_sweep_intro2": "Find the setting that best meets an objective across your recent cycles. Nothing changes until you apply it.", + "pg_sweep_no_metric": "Not enough data to score this objective.", "pg_sweep_progress": "Step {done} / {total}", "pg_sweep_step": "Step {done} / {total}", "pg_verdict_bad": "Needs attention: many cycles are going undetected.", @@ -930,7 +1007,7 @@ "preferences_admin": "Preferences, panel & access control", "preferences_adv": "Preferences", "prefs_personal": "These apply to your Home Assistant account only.", - "process_history_hint": "Re-run matching on all stored cycles, refresh tuning suggestions, retrain the ML models (if enabled), and recompute cycle health. Run this after a batch of reviews.", + "process_history_hint": "Re-run matching on all stored cycles, refresh tuning suggestions, retrain the ML models (if enabled), recost cycles against your recorded energy prices, and recompute cycle health. Run this after a batch of reviews.", "profile_deleted": "Profile deleted", "profile_poor_health_detail": "Cycles assigned to this profile have inconsistent shapes or low confidence. Consider rebuilding the envelope or reviewing labelled cycles.", "profiles_intro": "Click a profile for stats, phases and cleanup. Group near-identical programs (same shape/duration, different temperature or spin) so matching reliably picks between them.", @@ -999,6 +1076,7 @@ "toast_name_required": "Name required", "toast_no_automation": "Cannot create automation here", "toast_profile_name_required": "Profile name is required", + "toast_program_armed": "Program armed for the next cycle: {program}", "toast_program_set": "Program set: {program}", "toast_rebuild_failed": "Rebuild failed: {error}", "toast_resume_failed": "Resume failed: {error}", @@ -1043,10 +1121,12 @@ "anti_wrinkle_idle_timeout": "Quiet time allowed between two tumble pulses before anti-wrinkle ends", "dishwasher_end_spike_quiet_release": "Dishwasher: quiet seconds after expected duration before the end-of-cycle drain wait is released", "smart_termination_duration_ratio": "Fraction of the matched program's expected duration a cycle must reach before Smart Termination may end it early; lower it for load- or temperature-dependent machines", + "anti_crease_finalize_ratio": "Fraction of the matched program's expected duration a cycle must reach before the anti-crease tumble tail may be finalised; lower it on dryers with a long tumble tail, leave washing machines at the default", "anti_wrinkle_max_power": "A pulse above this ends anti-wrinkle and opens a new cycle", "anti_wrinkle_enabled": "Absorb the tumble pulses after the main phase instead of reading them as new cycles", "anti_wrinkle_max_duration": "A pulse longer than this ends anti-wrinkle and opens a new cycle", - "anti_wrinkle_exit_power": "Power must fall below this between pulses for anti-wrinkle to stay active" + "anti_wrinkle_exit_power": "Power must fall below this between pulses for anti-wrinkle to stay active", + "curve_preroll_seconds": "Seconds of readings from aborted start attempts carried into the front of a cycle's curve; 0 = off" }, "phase_desc": { "anti_crease": "Occasional short tumbles after completion to reduce wrinkles.", @@ -1140,6 +1220,10 @@ } }, "setting": { + "anti_crease_finalize_ratio": { + "doc": "How far into the matched program's expected duration a cycle must be before WashData may decide the low-power tumbling it is seeing is the anti-crease tail and finish the cycle there. The expected duration is the program's average, so on a dryer whose sensor-dry runtime follows the load, most runs never reach 98% of it and the tail sits until the fallback timeout instead - often 15 to 40 minutes after the machine actually stopped. Lower this (e.g. 0.75) on such a dryer. Leave washing machines at the default: a washer spends most of its cycle below the Max Anti-Wrinkle Power, so this fraction is the only thing separating a quiet mid-wash phase from the tail, and lowering it can cut a wash in half. Leave empty for the default (0.98). This is a different setting from the Smart Termination Ratio, which gates a separate early-finish check.", + "label": "Anti-Crease Finalize Ratio" + }, "anti_wrinkle_enabled": { "doc": "Recognise the short low-power tumble pulses a dryer emits after the main heat phase and keep them attached to the finished cycle instead of reading them as new cycles.", "label": "Enable Anti-Wrinkle Detection" @@ -1176,6 +1260,10 @@ "label": "Shape vs Level Weight", "doc": "Balance between curve shape and absolute power level when scoring a match. Higher favours shape; lower favours matching the power level." }, + "curve_preroll_seconds": { + "doc": "How many seconds of readings from aborted start attempts WashData may carry into the front of a cycle's curve. A cycle's curve normally begins at the start attempt that finally succeeds, so on a machine that probes a few times before settling - programme selection, door lock, first fill - the first minutes of real activity are missing from every curve. Set this a little above the longest such run-up your machine has (300 s is plenty for most). 0 turns it off. Be aware that turning it on moves the recorded start earlier, so cycles recorded before and after the change carry different durations for the same program; the learned averages will drift until the older cycles age out.", + "label": "Curve Pre-roll" + }, "delay_confirm_seconds": { "doc": "Power must stay in the standby band for this long before the appliance is treated as waiting-to-start rather than running.", "label": "Confirm Seconds" @@ -1256,8 +1344,12 @@ "doc": "Number of consecutive below-stop-threshold readings required before the cycle ends. 1 is fine for most plugs. Raise to 2-3 if your smart plug occasionally reports a false-zero sample mid-cycle and your cycles are ending prematurely.", "label": "End Repeat Count" }, + "energy_price_dynamic": { + "doc": "Charge each cycle at the price in force at every moment it ran, instead of the single price current when it finished. Only applies to a price entity (a static price cannot move). Needs no extra setup: the price is tracked live while the cycle runs, and Process History under Diagnostics recosts older cycles from the recorder. Off = the classic behaviour, one price frozen at the end.", + "label": "Time-Weighted Cost" + }, "energy_price_entity": { - "doc": "Sensor with the current electricity price per kWh (e.g. a dynamic tariff). Takes precedence over the static price below. Each cycle freezes the price in effect when it finished.", + "doc": "Sensor with the current electricity price per kWh (e.g. a dynamic tariff). Must be a price, not the plug's own power or energy entity: it takes precedence over the static price below, so a kWh counter here costs every cycle at the meter reading instead of your tariff. With Time-Weighted Cost on, each cycle is charged at the price in force at every moment it ran; otherwise the price in effect when it finished is frozen onto it.", "label": "Energy Price Entity" }, "energy_price_static": { @@ -1365,7 +1457,7 @@ "label": "Fire HA Events for Notifications" }, "notify_icon": { - "doc": "Optional mdi icon for the notification (e.g. mdi:washing-machine). Leave blank for the platform default.", + "doc": "The small icon shown on the notification (e.g. mdi:washing-machine). Android draws it in the status bar; iOS shows it in place of the app icon, and needs the companion app 2026.8 or newer. Leave blank for the platform default.", "label": "Notification Icon" }, "notify_live_chronometer": { @@ -1373,8 +1465,8 @@ "label": "Use Live Chronometer" }, "notify_live_click_action": { - "label": "Live Notification Tap Target", - "doc": "Android only. Where a tap on the live-progress notification opens (e.g. /lovelace/laundry, or a full URL) instead of the app landing page. Leave blank for the default." + "label": "Notification Tap Target", + "doc": "Where a tap on a notification from this appliance opens, on Android and iOS. Blank opens the WashData panel on this appliance. Enter a dashboard path (e.g. /lovelace/laundry) or a full URL to send it somewhere else, or \"none\" for no tap target at all." }, "notify_live_interval_seconds": { "doc": "How often live-progress notifications are refreshed while a cycle runs.", @@ -1388,6 +1480,10 @@ "doc": "notify.* services called for live progress updates while a cycle runs. Leave empty to disable live-progress notifications.", "label": "Live Progress Services" }, + "notify_live_silent": { + "label": "Silent Live Updates", + "doc": "iOS only. Refresh the live progress quietly: each update arrives without a sound or vibration, at a lower, battery-saving priority that iOS may briefly batch. The first update of a cycle (the one that starts the Live Activity) and the start/finish notifications still alert as usual. Turn this off to be alerted on every update." + }, "notify_live_sticky": { "label": "Keep Live Notification On Tap", "doc": "Android only. Make the live-progress notification persistent (sticky) so tapping it does not dismiss the ongoing thread. Off keeps the default behaviour where a tap dismisses it." @@ -1677,7 +1773,8 @@ "saving": "Saving…", "splitting": "Splitting…", "training": "Training…", - "trimming": "Trimming…" + "trimming": "Trimming…", + "unlabelling": "Unlabelling…" }, "suggestion": { "both_agree": "WashData recommends", @@ -1745,6 +1842,8 @@ "off_delay_generic_floor": "Used generic safe minimum ({floor}s).", "off_delay_pauses": "Sized to outlast real pauses: p95 intra-cycle pause {p95}s + 60s buffer, from {pauses} pauses across {cycles} clean cycles (floor {floor}s).", "match_interval": "Based on observed update cadence (median={median}s) * 10.", + "match_interval_capped": "Capped so {persistence} consecutive matches fit in {pct}% of the shortest program ({shortest}s); the update cadence (median={median}s) alone would have suggested a longer interval.", + "match_interval_floored": "The shortest program ({shortest}s) alone would cap this at {cap}s, below the {minimum}s minimum, so it is held there: {persistence} consecutive matches take {budget}s, which is more than {pct}% of that program. Matching only runs when a reading arrives (median={median}s), so the minimum costs nothing.", "tol_per_profile": "p75 of per-profile duration variance across {profiles} profiles ({cycles} cycles); tight profiles not penalised.", "tol_pooled": "Based on pooled duration variance of {cycles} recent labeled cycles (p95 dev={dev}).", "min_duration_ratio": "Kept as low as possible so a program is recognised early in the cycle; the confidence and ambiguity gates prevent premature commits.", @@ -1864,6 +1963,8 @@ "models_reverted": "On-device models reverted to baseline", "name_required": "Name required", "no_split_found": "No idle gaps found to split on", + "odometer_save_failed": "Could not save total: {error}", + "odometer_saved": "Total cycles saved", "panel_settings_saved": "Panel settings saved", "pause_no_cycle": "No active cycle to pause", "pg_preset_loaded": "Preset \"{name}\" loaded", @@ -1895,6 +1996,7 @@ "review_saved": "Review saved", "saved_except_conflicts": "Saved. Fix the highlighted conflicts to save the rest.", "settings_conflicts": "Fix the highlighted setting conflicts before saving.", + "settings_no_changes": "No changes to save", "settings_reverted": "Settings reverted; integration reloading", "settings_saved": "Settings saved; integration reloading", "share_device_none_sel": "Select at least one cycle to share", @@ -1924,7 +2026,9 @@ "suggestions_applied": "Suggestions applied; integration reloading", "suggestions_dismissed": "Suggestions dismissed", "task_cancelled": "Cancelled.", - "thanks_confirming": "Thanks for confirming" + "thanks_confirming": "Thanks for confirming", + "unlabel_done": "Unlabelled {count} cycle(s)", + "unlabel_failed": "Unlabel failed: {error}" }, "trend": { "down": "Trending down", @@ -1936,7 +2040,8 @@ "date_tip": "Date and time the cycle started.", "duration_tip": "Total cycle run time from start to end.", "energy_tip": "Total energy consumed (kWh). Computed by integrating power over time.", - "cost_tip": "Energy cost for this cycle, frozen at completion using the price in effect then (energy x price per kWh). Set a price under Settings to populate it.", + "cost_dynamic_tip": "Time-weighted: charged at the price in force during the cycle (effective {price} {cur}/kWh).", + "cost_tip": "Energy cost for this cycle, frozen at completion. With Time-Weighted Cost on, the power trace is charged at the price in force at each moment; otherwise energy x the single price in effect at the end. Set a price under Settings to populate it.", "confidence_tip": "Profile match confidence (0-100%). How closely the cycle power curve matched the identified program.", "health_tip": "ML cycle health (higher = better). Click a cycle to inspect and review it.", "flags_tip": "Review, anomaly and source flags for the cycle. Hover an icon for detail." @@ -1970,7 +2075,7 @@ "device": "Device", "favorites": "Favourites", "programs_count": "Programs: {n}", - "search_brand_ph": "Search by brand…", + "search_ph2": "Search by brand or model…", "search_ph": "Search by brand or model…", "your_model": "Yours", "your_model_tip": "This is the appliance you declared in Settings", @@ -2013,6 +2118,7 @@ "golden": "Reprocessing: backfilling golden", "suggestions": "Reprocessing: suggestions", "ml_training": "Reprocessing: ML training", + "costs": "Reprocessing: energy costs", "health": "Reprocessing: cycle health" }, "pg_sweep": { diff --git a/custom_components/ha_washdata/translations/panel/es.json b/custom_components/ha_washdata/translations/panel/es.json index 44c6c825..734c5304 100644 --- a/custom_components/ha_washdata/translations/panel/es.json +++ b/custom_components/ha_washdata/translations/panel/es.json @@ -1,5 +1,7 @@ { "badge": { + "quiet_tail_tip": "Cerca del final, este programa permanece en silencio unos {mins} min y luego consume unos {watts} W durante {secs} s antes de terminar. Visto en {seen} de {measured} ciclos medidos: el electrodoméstico no lo hace en todas las ejecuciones.", + "quiet_tail": "silencio final ~{mins} min", "artifact_tip": "{n} anomalías detectadas (p. ej., puerta abierta a mitad del ciclo): ábrala para verlas en el gráfico", "auto": "(detectado automáticamente)", "built_in_tag": "incorporado", @@ -15,7 +17,7 @@ "needs_review": "Necesita revisión", "overrun": "Corrió más de lo habitual", "poor_fit": "calidad deficiente", - "poor_fit_tip": "Historial de partidos inconsistente: considere reconstruir este perfil", + "poor_fit_tip": "Historial de coincidencias inconsistente: considere reconstruir este perfil", "restart_gap_tip": "{n} brecha de reinicio de HA: el trazado de potencia tiene un hueco", "reviewed": "Revisado", "steady": "→ estable", @@ -29,11 +31,14 @@ "approved": "Aprobado", "awaiting": "A la espera de aprobación", "imported_tip": "Importado de la tienda de la comunidad. Se usa solo para la coincidencia, no cuenta en las estadísticas.", + "unmatchable": "coincidencia imposible", "not_importable": "n/d aquí", "exists": "ya existe", - "backfilled_tip": "Detectado en un historial de potencia importado. Solo influye en la coincidencia de programas, no cuenta en las estadísticas." + "backfilled_tip": "Detectado en un historial de potencia importado. Solo influye en la coincidencia de programas, no cuenta en las estadísticas.", + "armed": "(se aplica al siguiente ciclo)" }, "btn": { + "reset_zoom": "Restablecer zoom", "set_brand_model": "Definir marca y modelo", "refresh_catalog": "Actualizar catálogo", "add_device": "+ Agregar dispositivo", @@ -167,6 +172,8 @@ "trim": "Recortar", "trim_split": "Recortar / Dividir", "undo": "Deshacer", + "unlabel_selected": "Quitar etiqueta a los seleccionados ({n})", + "unlabel_selected_tip": "Quita la etiqueta de perfil de los ciclos seleccionados. Permanecen en su historial y se podrán volver a etiquetar más tarde; solo dejan de influir en este perfil.", "use": "Usar", "wipe_all": "Borrar todos los datos", "delete_selected": "Eliminar seleccionados ({n})", @@ -174,7 +181,6 @@ "pg_apply_best": "Aplicar mejor", "pg_apply_to_settings": "Guardar en los ajustes", "pg_apply_to_settings_tip": "Copiar los valores que has editado aquí en los ajustes activos de este dispositivo", - "search": "Buscar", "import": "Importar", "disconnect": "Desconectar", "connect_github": "Conectar GitHub", @@ -201,7 +207,8 @@ "hist_read_recorder": "Leer desde Home Assistant", "hist_scan": "Buscar ciclos", "hist_import_n": "Importar {n} ciclos", - "hist_goto_cycles": "Ver los ciclos" + "hist_goto_cycles": "Ver los ciclos", + "save_total_cycles": "Guardar total" }, "conflict": { "anti_wrinkle_exit": { @@ -214,6 +221,12 @@ }, "attn_sub": "Corrige los conflictos antes de guardar", "attn_title": "Conflictos de ajustes: {n}", + "price_entity": { + "power_sensor": "Este es el Sensor de potencia del dispositivo, no un precio. Una entidad de precio anula el Precio de energía estático de abajo, por lo que los ciclos se cobrarían a vatios por kWh - borre este campo.", + "energy_sensor": "Esta es la Entidad del medidor de energía del dispositivo, no un precio. Una entidad de precio anula el Precio de energía estático de abajo, por lo que los ciclos se cobrarían a la lectura del medidor por kWh - borre este campo.", + "device_class": "Este sensor mide {dc}, no un precio por kWh. Una entidad de precio anula el Precio de energía estático de abajo - borre este campo o apúntelo a un sensor de tarifa.", + "unit": "Este sensor lee en {unit}, que no es un precio por kWh. Una entidad de precio anula el Precio de energía estático de abajo - borre este campo o apúntelo a un sensor de tarifa." + }, "settings_banner": "Conflictos de ajustes: {n}. Revisa las secciones resaltadas y corrígelos antes de guardar.", "settings_banner_btn": "Ir al primero", "confidence": { @@ -301,7 +314,7 @@ "pg_settings_source": "Origen de los ajustes", "phase_catalog": "Catálogo de fases", "playground": "Zona de pruebas", - "process_history": "Historia del proceso", + "process_history": "Procesar historial", "settings_history": "Historial de ajustes", "status": "Estado", "status_graph": "Gráfico de estado", @@ -312,12 +325,12 @@ "wipe_history": "Limpiar historial", "recommendations": "Recomendaciones ({n})", "pg_detection_params": "Ajustes de detección", - "pg_timeline": "Cronología de eventos", "pg_outcome": "Resultado de la simulación", "pg_across_cycles": "En todos tus ciclos", "community_store": "Tienda de la comunidad", "online_account": "Tienda de la comunidad y funciones en línea", - "import_power_history": "Importar historial de potencia" + "import_power_history": "Importar historial de potencia", + "service_status": "Estado del mantenimiento" }, "health": { "fair": "Calidad de perfil aceptable", @@ -325,6 +338,10 @@ "poor": "⚠ Calidad de perfil deficiente" }, "lbl": { + "envelope_position_tip": "Posición en la curva grabada del propio programa coincidente, {pct}%. Se mide alineando este ciclo con el perfil en lugar de contar el tiempo, así que sigue siendo correcta cuando un ciclo dura más o menos de lo habitual. Se actualiza mientras el electrodoméstico está en silencio, por lo que puede retrasarse durante una fase activa.", + "envelope_position": "curva {pct}%", + "aria_playground_chart3": "Gráfico interactivo de potencia del ciclo: pasa el cursor para leer el tiempo/la potencia, desplázate o pellizca para hacer zoom, arrastra para desplazarte", + "device_unresolved": "Dispositivo no disponible ({id})", "drag_to_resize": "Arrastre para cambiar el tamaño", "actions": "Comportamiento", "activity": "Actividad", @@ -460,7 +477,7 @@ "profile_index": "índice de perfil →", "profile_name": "Nombre del perfil", "program": "Programa", - "program_tip": "Anula qué perfil se asocia al ciclo actual. La detección automática permite que la integración elija la mejor coincidencia automáticamente. Fija un programa concreto para forzar la coincidencia cuando la detección automática falla o sabes qué está en marcha.", + "program_tip": "Anula qué perfil se asocia al ciclo actual. La detección automática permite que la integración elija la mejor coincidencia automáticamente. Fija un programa concreto para forzar la coincidencia cuando la detección automática falla o sabes qué está en marcha. Elige uno antes de poner en marcha el electrodoméstico y se aplicará en cuanto empiece el siguiente ciclo.", "progress": "Progreso", "raw_socket": "Señal bruta", "read_only": "Sólo lectura", @@ -486,7 +503,7 @@ "settings_basic": "Básico", "settings_detail_level": "Nivel de detalle de ajustes", "settings_overrides": "Anulaciones de ajustes", - "show_debug": "Mostrar tarjeta de depuración de partidos en vivo en la página de Estado (confianza, ambigüedad, mejores candidatos)", + "show_debug": "Mostrar la tarjeta de depuración de coincidencia en vivo en la página de Estado (confianza, ambigüedad, mejores candidatos)", "show_expected": "Mostrar superposición de curva esperada (perfil coincidente, naranja)", "show_raw": "Mostrar el interruptor de señal bruta del enchufe en el gráfico de potencia en vivo", "sparkline": "Tendencia reciente de duración de ciclos", @@ -495,6 +512,7 @@ "stage4": "Etapa 4 – concordancia", "status": "Estado", "tail_trim": "Recorte de fin (s)", + "tap_to_dismiss": "toca el gráfico para cerrar", "timer_auto_pause": "Pausa automática", "timer_min": "mín.", "timer_msg_placeholder": "Mensaje (opcional, {device}/{program}/{minutes})", @@ -531,9 +549,7 @@ "pg_time_left_model": "Tiempo restante (modelo)", "pg_time_left_model_tip": "Tiempo restante estimado por el modelo (estimador de fase + mezcla de ML), no una cuenta atrás fija", "phase": "Fase", - "pg_mode_simulate": "Simular", "pg_mode_history": "Probar en el historial", - "pg_mode_sweep": "Barrido", "pg_ev_detected": "Detectado", "pg_ev_match": "Coincidencia confirmada", "pg_ev_match_changed": "Coincidencia cambiada", @@ -572,14 +588,11 @@ "pg_stress_idle_w_desc": "Anula el nivel mínimo de reposo detectado automáticamente (dejar en blanco para automático).", "pg_stress_toggle": "Probar terminación en reposo", "pg_stress_toggle_desc": "Simula el electrodoméstico manteniéndose en su consumo en reposo tras el final de la grabación – muestra si WashData detiene el ciclo y cuándo.", - "pg_sweep_2d": "Mapa de calor 2D", - "pg_sweep_param_y": "2.º parámetro", "pg_obj_match": "Precisión de coincidencia", "pg_obj_endtiming": "Precisión del momento de fin", "pg_obj_falseend": "Tasa de falsos finales", "pg_obj_overrun": "Desviación de duración", "pg_obj_ambiguity": "Tasa de ambigüedad", - "pg_best_combo": "mejor combinación", "pg_mode_optimize": "Optimizar", "pg_group_matching": "Coincidencia de programa", "pg_zoom_hint": "haz doble clic para restablecer el zoom", @@ -658,7 +671,12 @@ "task_history_import_apply": "Importando ciclos", "evidence_real_cycles": "Ciclos que ejecutó esta máquina", "evidence_reference_cycles": "Descargados de la tienda de la comunidad", - "evidence_backfill_cycles": "Encontrados en un historial de potencia importado" + "evidence_backfill_cycles": "Encontrados en un historial de potencia importado", + "zoom_hint_touch": "pellizcar para hacer zoom", + "zoom_hint_touch_zoomed": "pellizcar para hacer zoom · doble toque para restablecer", + "cycles_since_service": "{since} / {total} ciclos", + "total_cycles_run": "ciclos realizados en total", + "correct_total_cycles": "Corregir el total" }, "log": { "all_levels": "Todos los niveles", @@ -708,7 +726,7 @@ "merge_cycles": "Fusionar {n} ciclos", "new_group": "Nuevo grupo de perfiles", "new_phase": "Nueva fase", - "process_history_msg": "Vuelva a ejecutar la coincidencia, actualice las sugerencias, vuelva a entrenar los modelos ML (si están habilitados) y recalcule el estado de todos los ciclos almacenados. Esto puede tardar un momento.", + "process_history_msg": "Vuelva a ejecutar la coincidencia, actualice las sugerencias, vuelva a entrenar los modelos ML (si están habilitados), vuelva a calcular el costo de los ciclos según los precios de energía registrados y recalcule el estado de todos los ciclos almacenados. Esto puede tardar un momento.", "process_history_ok": "Procesar", "process_history_title": "Procesar historial", "process_recording": "Grabación de procesos", @@ -741,6 +759,7 @@ "history_import": "Importar historial de potencia" }, "msg": { + "pg_canvas_empty3": "Elige un ciclo de arriba y pulsa Ejecutar para simularlo. Después, pasa el cursor para leer los valores, desplázate o pellizca para hacer zoom y arrastra para desplazarte.", "tail_trim_hint": "Número de segundos que se eliminarán del final", "store_sibling_hint": "¿No hay nada compartido para su modelo exacto? Un modelo muy parecido de la misma marca suele ser un buen punto de partida.", "store_declare_appliance": "Indique a WashData qué electrodoméstico tiene y esta pestaña mostrará las configuraciones que otras personas han compartido para él. También puede escribir una marca arriba para echar un vistazo.", @@ -754,7 +773,7 @@ "artifact_spike_detail": "Superó la banda de potencia habitual durante ~{n}s.", "auto_label_intro": "Asigne perfiles a ciclos sin etiquetar cuya confianza de coincidencia supere el umbral.", "automations_intro": "WashData dispara los eventos {start} / {end} y expone entidades, por lo que las notificaciones y acciones se construyen mejor como automatizaciones normales de Home Assistant. Las automatizaciones que usan este dispositivo aparecen a continuación.", - "cleanup_intro": "Cada ciclo etiquetado se superpone. Marque los valores atípicos y elimínelos para limpiar el perfil.", + "cleanup_intro": "Cada ciclo etiquetado se superpone. Marque los valores atípicos y luego quíteles la etiqueta (se conservan en el historial, pero dejan de influir en este perfil) o elimínelos definitivamente.", "clear_debug_hint": "Elimine los datos de depuración almacenados para liberar espacio.", "collecting_data": "Recopilando datos. Ciclos que faltan para que pueda comenzar el ajuste fino: {need} ({current}/{min}).", "compare_overlay_profiles": "Perfiles superpuestos (débil)", @@ -853,7 +872,7 @@ "preferences_admin": "Preferencias, panel y control de acceso", "preferences_adv": "Preferencias", "prefs_personal": "Estos se aplican únicamente a su cuenta de Home Assistant.", - "process_history_hint": "Vuelva a ejecutar la coincidencia en todos los ciclos almacenados, actualice las sugerencias de ajuste, vuelva a entrenar los modelos ML (si están habilitados) y recalcule el estado de los ciclos. Ejecútelo después de un lote de revisiones.", + "process_history_hint": "Vuelva a ejecutar la coincidencia en todos los ciclos almacenados, actualice las sugerencias de ajuste, vuelva a entrenar los modelos ML (si están habilitados), vuelva a calcular el costo de los ciclos según los precios de energía registrados y recalcule el estado de los ciclos. Ejecútelo después de un lote de revisiones.", "profile_deleted": "Perfil eliminado", "profile_poor_health_detail": "Los ciclos asignados a este perfil tienen formas inconsistentes o baja confianza. Considere reconstruir el sobre o revisar los ciclos etiquetados.", "profiles_intro": "Haga clic en un perfil para ver estadísticas, fases y limpieza. Agrupe programas casi idénticos (misma forma/duración, diferente temperatura o centrifugado) para que al combinarlos se elija de manera confiable.", @@ -936,9 +955,10 @@ "advisory_poor_health": "'{name}' tiene una baja puntuación de coincidencia - sus ciclos recientes varían mucho o coinciden poco. Revisa sus ciclos o vuelve a grabar el perfil para que el reconocimiento y las estimaciones de tiempo sigan siendo precisos.", "advisory_shape_drift": "'{name}' se ha alejado notablemente de su curva de potencia original. Puede que el electrodoméstico haya cambiado de comportamiento con el tiempo (p. ej. cal, desgaste). Considera volver a grabar este perfil con ciclos recientes.", "advisory_shape_drift_corr": "'{name}' se ha alejado notablemente de su curva de potencia original (correlación {corr}). Puede que el electrodoméstico haya cambiado de comportamiento con el tiempo (p. ej. cal, desgaste). Considera volver a grabar este perfil con ciclos recientes.", + "advisory_unmatchable": "'{name}' nunca podrá coincidir: no tiene detrás ningún ciclo con datos de potencia, por lo que WashData no tiene nada con lo que comparar un ciclo en curso. Etiquete un ciclo de este programa (o registre uno) para volver a incluirlo en la coincidencia.", + "advisory_unmatchable_no_duration": "'{name}' nunca podrá coincidir: tiene un ciclo con datos de potencia pero sin una duración utilizable, por lo que WashData no dispone de ninguna duración con la que dimensionar una coincidencia. Vuelva a registrar este programa, o indique su duración prevista, para volver a incluirlo en la coincidencia.", "pg_no_alerts": "No se detectaron problemas en esta ejecución.", "pg_override_active": "Usando sus ajustes editados vs. los actuales", - "pg_history_intro": "Reproduzca sus ciclos recientes con el detector y el comparador reales. Edite primero un ajuste de detección en Simular para ver aquí una comparación antes/después.", "pg_history_empty": "Pulse Ejecutar para reproducir sus ciclos.", "pg_history_summary": "{detected}/{total} detectados · {correct} reconocidos correctamente", "pg_sweep_no_metric": "No hay datos suficientes para puntuar este objetivo.", @@ -1015,11 +1035,16 @@ "hist_scan_failed": "El análisis falló.", "hist_scan_expired": "Ese análisis ya no está disponible. Vuelva a analizar el historial.", "hist_import_failed": "La importación falló.", - "imported_history_readonly": "Detectado en un historial de potencia importado. Influye en la coincidencia de programas, pero no cuenta en sus estadísticas y no se puede recortar ni dividir. Etiquételo para indicar el programa." + "imported_history_readonly": "Detectado en un historial de potencia importado. Influye en la coincidencia de programas, pero no cuenta en sus estadísticas y no se puede recortar ni dividir. Etiquételo para indicar el programa.", + "odometer_intro": "Este total solo aumenta. Eliminar el registro de un ciclo no lo modifica y sigue contando más allá del límite del historial almacenado, de modo que los recordatorios de servicio siguen siendo correctos.", + "odometer_correct_hint": "Ajuste este valor si WashData registró un ciclo que nunca ocurrió, o si el electrodoméstico ya había realizado ciclos antes de instalar WashData.", + "no_reminders_set": "Aún no hay recordatorios de servicio configurados.", + "toast_program_armed": "Programa preparado para el siguiente ciclo: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Lavavajillas: segundos de silencio tras la duración esperada antes de liberar la espera del vaciado de fin de ciclo", "smart_termination_duration_ratio": "Fracción de la duración esperada del programa coincidente que un ciclo debe alcanzar antes de que la Terminación inteligente pueda finalizarlo antes de tiempo; redúzcala para máquinas que dependen de la carga o la temperatura", + "anti_crease_finalize_ratio": "Fracción de la duración esperada del programa coincidente que un ciclo debe alcanzar antes de que pueda finalizarse la cola de volteo antiarrugas; redúzcala en secadoras con una cola de volteo larga, deje las lavadoras en el valor predeterminado", "anti_wrinkle_enabled": "Absorber los pulsos de volteo tras la fase principal en lugar de leerlos como nuevos ciclos", "anti_wrinkle_exit_power": "La potencia debe caer por debajo de este valor entre pulsos para que el modo antiarrugas siga activo", "anti_wrinkle_idle_timeout": "Tiempo de silencio permitido entre dos pulsos de volteo antes de que finalice el modo antiarrugas", @@ -1045,7 +1070,8 @@ "dtw_ddtw_scale": "Etapa 3: distancia de semisaturación DDTW; menor = mayor sensibilidad a la forma (predeterminado 30)", "dtw_refine_top_n": "Etapa 3: candidatos que DTW vuelve a puntuar; aumente a 7-9 si el perfil correcto se sitúa en 4.º-5.º lugar (predeterminado 5)", "duration_scale": "Etapa 4: log-razón donde la concordancia de duración se reduce a la mitad; menor = penalización más estricta (predeterminado 0,175)", - "energy_scale": "Etapa 4: log-razón donde la concordancia de energía se reduce a la mitad; menor = penalización más estricta (predeterminado 0,25)" + "energy_scale": "Etapa 4: log-razón donde la concordancia de energía se reduce a la mitad; menor = penalización más estricta (predeterminado 0,25)", + "curve_preroll_seconds": "Segundos de lecturas de intentos de arranque interrumpidos incorporados al principio de la curva de un ciclo; 0 = desactivado" }, "phase_desc": { "anti_crease": "Volteos breves ocasionales tras finalizar para evitar que la ropa se arrugue.", @@ -1143,6 +1169,10 @@ "label": "Liberación en silencio del secado pasivo", "doc": "Una vez que el ciclo supera su duración esperada, cuánto tiempo debe permanecer en silencio el lavavajillas (por debajo del Umbral de parada) antes de que WashData deje de esperar un vaciado final y finalice el ciclo. Auméntelo si su máquina tiene una larga fase de secado silenciosa antes de un vaciado final tardío que no se detecta - una ventana más amplia permite que la duración aprendida siga la deriva estacional (agua de entrada más fría = ciclos más largos) en lugar de fijarse en la media anterior. Solo acorta la espera respecto al tope interno de 30 minutos para el pico de fin, nunca la prolonga." }, + "anti_crease_finalize_ratio": { + "label": "Proporción de finalización antiarrugas", + "doc": "Hasta qué punto de la duración esperada del programa coincidente debe haber llegado un ciclo antes de que WashData pueda decidir que el volteo de baja potencia que está viendo es la cola antiarrugas y dar ahí por terminado el ciclo. La duración esperada es el promedio del programa, así que en una secadora cuyo tiempo de secado por sonda depende de la carga la mayoría de los ciclos nunca alcanzan el 98 % de ese promedio y la cola se queda esperando hasta el tiempo de espera de reserva, a menudo de 15 a 40 minutos después de que la máquina se detuviera realmente. Baje este valor (por ejemplo, 0,75) en una secadora así. Deje las lavadoras en el valor predeterminado: una lavadora pasa la mayor parte de su ciclo por debajo de la Potencia máxima antiarrugas, por lo que esta fracción es lo único que separa una fase tranquila a mitad del lavado de la cola, y reducirla puede cortar un lavado por la mitad. Déjelo vacío para el valor predeterminado (0,98). Este ajuste es distinto de la Proporción de terminación inteligente, que regula una comprobación de finalización anticipada aparte." + }, "anti_wrinkle_enabled": { "doc": "Reconozca los impulsos cortos de baja potencia que emite una secadora después de la fase de calentamiento principal y manténgalos adjuntos al ciclo finalizado en lugar de leerlos como ciclos nuevos.", "label": "Habilitar detección antiarrugas" @@ -1164,7 +1194,7 @@ "label": "Potencia máxima antiarrugas" }, "auto_label_confidence": { - "doc": "Si la puntuación del partido al final del ciclo es igual o superior a este valor, el programa se etiqueta automáticamente sin ningún mensaje de confirmación. Auméntelo para exigir una mayor certeza antes del etiquetado automático; bájelo para automatizar más. Funciona en conjunto con Learning Confidence debajo de él.", + "doc": "Si la puntuación de coincidencia al final del ciclo es igual o superior a este valor, el programa se etiqueta automáticamente sin ningún mensaje de confirmación. Auméntelo para exigir una mayor certeza antes del etiquetado automático; bájelo para automatizar más. Funciona en conjunto con la Confianza de aprendizaje que aparece debajo.", "label": "Confianza de etiquetado automático" }, "auto_maintenance": { @@ -1175,6 +1205,10 @@ "doc": "Los ciclos más cortos que este se descartan como ciclos fantasma (ejecuciones de prueba, apertura de la puerta para agregar un calcetín).", "label": "Duración mínima del ciclo" }, + "curve_preroll_seconds": { + "label": "Preinicio de la curva", + "doc": "Cuántos segundos de lecturas procedentes de intentos de arranque interrumpidos puede incorporar WashData al principio de la curva de un ciclo. La curva de un ciclo empieza normalmente en el intento de arranque que por fin tiene éxito, así que en una máquina que tantea varias veces antes de arrancar de verdad (selección del programa, bloqueo de la puerta, primer llenado) faltan los primeros minutos de actividad real en todas las curvas. Ajuste este valor un poco por encima del arranque previo más largo de su máquina (300 s sobran en la mayoría de los casos). 0 lo desactiva. Tenga en cuenta que activarlo adelanta el inicio registrado, de modo que los ciclos registrados antes y después del cambio tienen duraciones distintas para el mismo programa; las medias aprendidas se desviarán hasta que los ciclos antiguos salgan del historial." + }, "delay_confirm_seconds": { "doc": "La energía debe permanecer en la banda de espera durante este tiempo antes de que el electrodoméstico sea considerado como esperando para iniciarse en lugar de funcionando.", "label": "Segundos de confirmación" @@ -1212,7 +1246,7 @@ "label": "Recuento de repetición de fin" }, "energy_price_entity": { - "doc": "Sensor con el precio actual de la electricidad por kWh (por ejemplo, una tarifa dinámica). Tiene prioridad sobre el precio estático a continuación. Cada ciclo congela el precio vigente cuando finaliza.", + "doc": "Sensor con el precio actual de la electricidad por kWh (por ejemplo, una tarifa dinámica). Debe ser un precio, no la propia entidad de potencia o de energía del enchufe: tiene prioridad sobre el precio estático a continuación, por lo que un contador de kWh aquí cobra cada ciclo a la lectura del medidor en lugar de a su tarifa. Con Costo ponderado en el tiempo activado, cada ciclo se cobra al precio vigente en cada momento de su ejecución; si no, se le congela el precio vigente cuando terminó.", "label": "Entidad de precio de energía" }, "energy_price_static": { @@ -1240,7 +1274,7 @@ "label": "Invertir disparador externo (disparar en APAGADO)" }, "learning_confidence": { - "doc": "Si la puntuación de coincidencia se sitúa entre esta y la confianza de la etiqueta automática, una notificación de comentarios le pedirá que verifique el programa identificado. Por debajo de este marcador, el partido es demasiado incierto para salir a la luz. Debe mantenerse por debajo del nivel de confianza de la etiqueta automática.", + "doc": "Si la puntuación de coincidencia se sitúa entre esta y la Confianza de etiquetado automático, una notificación de comentarios le pedirá que verifique el programa identificado. Por debajo de esta puntuación, la coincidencia es demasiado incierta para mostrarse. Debe mantenerse por debajo de la Confianza de etiquetado automático.", "label": "Confianza de aprendizaje" }, "linked_device": { @@ -1308,7 +1342,7 @@ "label": "Disparar eventos de HA para notificaciones" }, "notify_icon": { - "doc": "Icono mdi opcional para la notificación (por ejemplo, mdi: lavadora). Déjelo en blanco para la plataforma predeterminada.", + "doc": "El pequeño icono que se muestra en la notificación (por ejemplo, mdi:washing-machine). Android lo dibuja en la barra de estado; iOS lo muestra en lugar del icono de la aplicación y necesita la aplicación complementaria 2026.8 o posterior. Déjelo en blanco para el valor predeterminado de la plataforma.", "label": "Icono de notificación" }, "notify_live_chronometer": { @@ -1408,7 +1442,7 @@ "label": "Umbral de coincidencia" }, "profile_unmatch_threshold": { - "doc": "Si un partido en vivo a mitad de ciclo cae por debajo de este puntaje, se borra la identificación tentativa. Manténgalo un poco por debajo del Umbral de coincidencia para que una breve caída en la similitud no haga que la pantalla vuelva a ser incomparable.", + "doc": "Si una coincidencia en vivo a mitad de ciclo cae por debajo de esta puntuación, se borra la identificación provisional. Manténgalo un poco por debajo del Umbral de coincidencia para que una breve caída de la similitud no devuelva la pantalla al estado sin coincidencia.", "label": "Umbral de no coincidencia" }, "progress_reset_delay": { @@ -1419,10 +1453,6 @@ "doc": "Segundos que una bomba puede funcionar continuamente antes de que se marque como posiblemente atascada (activa el evento de bomba atascada).", "label": "Duración de bomba atascada" }, - "running_dead_zone": { - "doc": "Después de que comienza un ciclo, se ignoran las caídas de energía dentro de esta ventana. Las lavadoras se llenan con agua fría (que cae cerca de 0 W antes de calentarse); sin esta protección, la fase de llenado parece un final de ciclo. Esto NO omite datos: el trazo de potencia total se registra desde T=0. El motor de sugerencias mide el patrón de inicio real de su máquina y lo dimensiona automáticamente.", - "label": "Zona muerta en funcionamiento" - }, "sampling_interval": { "doc": "Tiempo esperado entre lecturas del sensor: se utiliza para dimensionar la ventana de suavizado e iniciar el rebote correctamente. Cada actualización del sensor se captura independientemente de este valor; solo calibra los cálculos posteriores. El motor de sugerencias mide la cadencia real de su sensor de ciclos anteriores y la configura automáticamente.", "label": "Intervalo de muestreo" @@ -1440,7 +1470,7 @@ "label": "Ventana de suavizado" }, "start_duration_threshold": { - "doc": "La energía debe permanecer por encima del umbral de inicio durante este tiempo para confirmar un inicio real, evitando que los cambios de encendido/apagado en fracciones de segundo inicien un ciclo.", + "doc": "La potencia debe permanecer por encima del Umbral de inicio durante este tiempo para confirmar un inicio real, evitando que los cambios de encendido/apagado en fracciones de segundo inicien un ciclo.", "label": "Duración de inicio" }, "start_energy_threshold": { @@ -1448,7 +1478,7 @@ "label": "Energía de inicio" }, "start_threshold_w": { - "doc": "La energía debe superar este nivel para confirmar que ha comenzado un ciclo. Configurarlo demasiado bajo provoca arranques en falso desde la energía de reserva; Se omiten programas de arranque demasiado altos y lentos (llenado en frío). El motor de sugerencias lo establece justo por encima de la potencia activa más baja observada en la máquina.", + "doc": "La potencia debe superar este nivel para confirmar que ha comenzado un ciclo. Configurarlo demasiado bajo provoca arranques en falso por el consumo en espera; configurarlo demasiado alto hace que se pierdan los programas de arranque lento (llenado en frío). El motor de sugerencias lo establece justo por encima de la potencia activa más baja observada en la máquina.", "label": "Umbral de inicio" }, "stop_threshold_w": { @@ -1550,13 +1580,17 @@ "label": "Intervalo del perfil de potencia", "doc": "Tamaño del intervalo para el atributo del sensor power_profile por perfil (la matriz plana de vatios medios por franja, consumida por planificadores externos como EMHASS y tibber_prices). Intervalos más pequeños mantienen nítidos los picos de potencia breves; intervalos más grandes suavizan la forma. Predeterminado 15 min. Solo de lectura; no afecta a la detección." }, + "notify_live_silent": { + "label": "Actualizaciones en vivo silenciosas", + "doc": "Solo iOS. Actualiza el progreso en vivo de forma discreta: cada actualización llega sin sonido ni vibración, con una prioridad más baja que ahorra batería y que iOS puede agrupar durante unos instantes. La primera actualización de un ciclo (la que inicia la Actividad en vivo) y las notificaciones de inicio y de fin siguen avisando como siempre. Desactívelo para recibir un aviso en cada actualización." + }, "notify_live_sticky": { "label": "Mantener la notificación activa al pulsar", "doc": "Solo Android. Hace que la notificación de progreso en vivo sea persistente (fija), de modo que pulsarla no la cierre. Desactivado mantiene el comportamiento predeterminado en el que un toque la cierra." }, "notify_live_click_action": { - "label": "Destino al pulsar la notificación en vivo", - "doc": "Solo Android. Destino que se abre al pulsar la notificación de progreso en vivo (por ejemplo, /lovelace/lavandería, o una URL completa), en lugar de la página de inicio de la aplicación. Dejar en blanco para el valor predeterminado." + "label": "Destino al pulsar la notificación", + "doc": "Destino que se abre al pulsar una notificación de este electrodoméstico, tanto en Android como en iOS. En blanco abre el panel de WashData en este electrodoméstico. Introduzca la ruta de un tablero (por ejemplo, /lovelace/laundry) o una URL completa para abrir otro destino, o «none» para que no se abra nada al pulsar." }, "door_opens_at_end": { "label": "La puerta se abre automáticamente al final", @@ -1569,6 +1603,10 @@ "profile_evidence_sources": { "label": "Ciclos que forman un programa", "doc": "Qué ciclos se usan para construir la curva de potencia de cada programa y para comparar con ella un ciclo terminado. Al desmarcar un tipo, este deja de dar forma a sus programas sin que se borre nada - los ciclos siguen en su lista de Ciclos y aún se pueden etiquetar o eliminar. Útil si no confía en los datos importados. Las estadísticas no se ven afectadas: siempre cuentan solo los ciclos que esta máquina ejecutó realmente. Desmarcar todo se ignora, ya que un programa sin ciclos detrás nunca podría coincidir." + }, + "energy_price_dynamic": { + "label": "Costo ponderado en el tiempo", + "doc": "Cobra cada ciclo al precio vigente en cada momento de su ejecución, en vez de aplicarle el único precio del instante en que terminó. Solo se aplica a una entidad de precio (un precio estático no puede variar). No necesita configuración adicional: el precio se sigue en vivo mientras el ciclo avanza, y Procesar historial, en Diagnóstico, vuelve a calcular el costo de los ciclos antiguos a partir del recorder. Desactivado = el comportamiento clásico, un solo precio congelado al final." } }, "setting_group": { @@ -1680,7 +1718,8 @@ "splitting": "Dividiendo…", "deleting": "Eliminando…", "imported": "Importado", - "preparing": "Preparando…" + "preparing": "Preparando…", + "unlabelling": "Quitando etiqueta…" }, "tab": { "advanced": "Avanzado", @@ -1764,6 +1803,7 @@ "recording_stopped": "Grabación detenida", "review_saved": "Revisión guardada", "settings_conflicts": "Corrija los conflictos de ajustes resaltados antes de guardar.", + "settings_no_changes": "No hay cambios para guardar", "settings_reverted": "Configuración revertida; recarga de integración", "settings_saved": "Configuración guardada; recarga de integración", "suggestions_applied": "Sugerencias aplicadas; recarga de integración", @@ -1818,7 +1858,11 @@ "export_selective_done": "Exportación descargada", "import_selective_done": "Se importaron {profiles} perfil(es) y {cycles} ciclo(s)", "hist_csv_required": "Cargue primero un archivo CSV o pegue su contenido", - "file_read_failed": "No se pudo leer ese archivo" + "file_read_failed": "No se pudo leer ese archivo", + "unlabel_done": "Etiqueta quitada de {count} ciclo(s)", + "unlabel_failed": "Error al quitar la etiqueta: {error}", + "odometer_saved": "Total de ciclos guardado", + "odometer_save_failed": "No se pudo guardar el total: {error}" }, "suggestion": { "both_agree": "WashData recomienda", @@ -1865,10 +1909,6 @@ "higher": "Solo confirma coincidencias de alta confianza; más ciclos quedan sin etiquetar", "lower": "Coincide con más ciclos; algunos pueden ser ligeramente incorrectos" }, - "running_dead_zone": { - "higher": "Ignora breves picos de potencia en reposo – menos sensible al ruido", - "lower": "Reacciona a impulsos más cortos – detecta actividad transitoria rápida" - }, "start_threshold_w": { "higher": "Evita arranques falsos por breves picos de potencia", "lower": "Detecta programas de baja potencia; puede activarse con ruido" @@ -1893,6 +1933,8 @@ "end_repeat_count": "{false}/{total} ciclos limpios ({pct}%) tuvieron una pausa interna >60s que se reanudó; se requieren {val} confirmación(es) de fin.", "learning_confidence": "Confianza p05 de {cycles} ciclos etiquetados por el usuario ({p05}); por debajo, se solicita verificación.", "match_interval": "Según la cadencia de actualización observada (mediana={median}s) * 10.", + "match_interval_capped": "Limitado para que {persistence} coincidencias consecutivas quepan en el {pct}% del programa más corto ({shortest}s); la cadencia de actualización por sí sola (mediana={median}s) habría sugerido un intervalo más largo.", + "match_interval_floored": "El programa más corto ({shortest}s) por sí solo limitaría este valor a {cap}s, por debajo del mínimo de {minimum}s, así que se mantiene ahí: {persistence} coincidencias consecutivas tardan {budget}s, más del {pct}% de ese programa. La coincidencia solo se ejecuta cuando llega una lectura (mediana={median}s), por lo que el mínimo no cuesta nada.", "max_duration_ratio": "Según las duraciones de los ciclos etiquetados (p95={p95}).", "min_active": "Según la potencia activa mínima ({min}W) observada en el último ciclo.", "min_duration_ratio": "Se mantiene lo más bajo posible para que un programa se reconozca pronto en el ciclo; los umbrales de confianza y ambigüedad evitan decisiones prematuras.", @@ -1950,10 +1992,11 @@ "date_tip": "Fecha y hora de inicio del ciclo.", "duration_tip": "Tiempo total de ejecución del ciclo, de principio a fin.", "energy_tip": "Energía total consumida (kWh). Calculada integrando la potencia a lo largo del tiempo.", - "cost_tip": "Costo de energía de este ciclo, congelado al finalizar usando el precio vigente en ese momento (energía x precio por kWh). Establezca un precio en Ajustes para completarlo.", + "cost_tip": "Costo de energía de este ciclo, congelado al finalizar. Con Costo ponderado en el tiempo activado, la curva de potencia se cobra al precio vigente en cada momento; si no, energía x el único precio vigente al final. Establezca un precio en Ajustes para completarlo.", "confidence_tip": "Confianza de coincidencia del perfil (0-100%). Qué tan cerca coincidió la curva de potencia del ciclo con el programa identificado.", "health_tip": "Salud del ciclo por ML (más alto = mejor). Haga clic en un ciclo para inspeccionarlo y revisarlo.", - "flags_tip": "Indicadores de revisión, anomalía y origen del ciclo. Pase el cursor sobre un icono para ver los detalles." + "flags_tip": "Indicadores de revisión, anomalía y origen del ciclo. Pase el cursor sobre un icono para ver los detalles.", + "cost_dynamic_tip": "Ponderado en el tiempo: cobrado al precio vigente durante el ciclo (precio efectivo {price} {cur}/kWh)." }, "stat": { "avg": "prom.", @@ -1980,9 +2023,9 @@ "finished": "El ciclo alcanzó un estado final y terminó." }, "store": { + "search_ph2": "Buscar por marca o modelo…", "your_model_tip": "Este es el electrodoméstico que declaró en Ajustes", "your_model": "El suyo", - "search_brand_ph": "Buscar por marca…", "programs_count": "Programas: {n}", "browse": "Explorar", "device": "Dispositivo", @@ -2027,7 +2070,8 @@ "golden": "Reprocesando: marcando ciclos de referencia", "suggestions": "Reprocesando: sugerencias", "ml_training": "Reprocesando: entrenamiento de ML", - "health": "Reprocesando: salud de los ciclos" + "health": "Reprocesando: salud de los ciclos", + "costs": "Reprocesando: costos de energía" }, "pg_sweep": { "optimize": "Optimizar: {param}" diff --git a/custom_components/ha_washdata/translations/panel/et.json b/custom_components/ha_washdata/translations/panel/et.json index 16c6a851..e6b28c6c 100644 --- a/custom_components/ha_washdata/translations/panel/et.json +++ b/custom_components/ha_washdata/translations/panel/et.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Lõpu poole jääb see programm umbes {mins} minutiks vaikseks ja tarbib siis enne lõpetamist {secs} s jooksul umbes {watts} W. Täheldatud {seen} korral {measured} mõõdetud tsüklist: seade ei tee seda igal käivitusel.", + "quiet_tail": "vaikne lõpp ~{mins} min", "artifact_tip": "Tuvastati {n} anomaaliat (nt uks avati tsükli keskel) – avage, et näha neid graafikul", "auto": "(tuvastatud automaatselt)", "built_in_tag": "sisseehitatud", @@ -63,7 +65,7 @@ "needs_review": "Vajab ülevaatamist", "overrun": "Jooks kauem kui tavaliselt", "poor_fit": "kehv kvaliteet", - "poor_fit_tip": "Ebajärjekindel mänguajalugu – kaaluge selle profiili uuesti ülesehitamist", + "poor_fit_tip": "Ebajärjekindel sobitamise ajalugu – kaaluge selle profiili uuesti ülesehitamist", "restart_gap_tip": "{n} HA taaskäivituslünk: võimsusjälg on katkenud", "reviewed": "Üle vaadatud", "steady": "→ stabiilne", @@ -77,11 +79,14 @@ "approved": "Heaks kiidetud", "awaiting": "Ootab heakskiitu", "imported_tip": "Imporditud kogukonna poest. Kasutatakse ainult sobitamiseks, ei arvestata statistikas.", + "unmatchable": "ei saa sobitada", "not_importable": "pole saadaval", "exists": "on olemas", - "backfilled_tip": "Tuvastatud imporditud võimsuse ajaloos. Mõjutab ainult programmide sobitamist, statistikas ei arvestata." + "backfilled_tip": "Tuvastatud imporditud võimsuse ajaloos. Mõjutab ainult programmide sobitamist, statistikas ei arvestata.", + "armed": "(kehtib järgmise tsükli kohta)" }, "btn": { + "reset_zoom": "Lähtesta suum", "set_brand_model": "Määra bränd ja mudel", "refresh_catalog": "Värskenda kataloogi", "add_device": "+ Lisa seade", @@ -215,6 +220,8 @@ "trim": "Kärbi", "trim_split": "Kärbi / Poolita", "undo": "Võta tagasi", + "unlabel_selected": "Eemalda valitute märgistus ({n})", + "unlabel_selected_tip": "Eemaldab profiilimärgistuse valitud tsüklitelt. Need jäävad sinu ajalukku ja neid saab hiljem uuesti märgistada - nad lihtsalt ei kujunda enam seda profiili.", "use": "Kasutage", "wipe_all": "Pühkige kõik andmed", "delete_selected": "Kustuta valitud ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Rakenda parim", "pg_apply_to_settings": "Salvesta sätetesse", "pg_apply_to_settings_tip": "Kopeeri siin muudetud väärtused selle seadme aktiivsetesse sätetesse", - "search": "Otsi", "import": "Impordi", "disconnect": "Katkesta ühendus", "connect_github": "Ühenda GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Loe Home Assistantist", "hist_scan": "Otsi tsükleid", "hist_import_n": "Impordi tsüklid ({n})", - "hist_goto_cycles": "Näita tsükleid" + "hist_goto_cycles": "Näita tsükleid", + "save_total_cycles": "Salvesta koguarv" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Lahendage konfliktid enne salvestamist", "attn_title": "Seadete konfliktid: {n}", + "price_entity": { + "power_sensor": "See on seadme Võimsusandur, mitte hind. Hinna olem on ülimuslik allpool oleva staatilise hinna suhtes, seega arvestataks tsüklite maksumus nii, nagu oleks vattide näit hind kWh kohta - tühjendage see väli.", + "energy_sensor": "See on seadme Energiaarvesti, mitte hind. Hinna olem on ülimuslik allpool oleva staatilise hinna suhtes, seega arvestataks tsüklite maksumus nii, nagu oleks arvesti näit hind kWh kohta - tühjendage see väli.", + "device_class": "See andur mõõdab suurust {dc}, mitte hinda kWh kohta. Hinna olem on ülimuslik allpool oleva staatilise hinna suhtes - tühjendage see väli või valige tariifiandur.", + "unit": "Selle anduri näidu ühik on {unit}, mis ei ole hind kWh kohta. Hinna olem on ülimuslik allpool oleva staatilise hinna suhtes - tühjendage see väli või valige tariifiandur." + }, "settings_banner": "Seadete konfliktid: {n}. Kontrollige esile tõstetud sektsioone ja parandage need enne salvestamist.", "settings_banner_btn": "Mine esimesele", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Seadete allikas", "phase_catalog": "Faaside kataloog", "playground": "Katseala", - "process_history": "Protsessi ajalugu", + "process_history": "Töötle ajalugu", "settings_history": "Seadete ajalugu", "status": "Olek", "status_graph": "Olekugraafik", @@ -360,12 +373,12 @@ "wipe_history": "Pühkige ajalugu", "recommendations": "Soovitused ({n})", "pg_detection_params": "Tuvastuse seaded", - "pg_timeline": "Sündmuste ajajoon", "pg_outcome": "Simulatsiooni tulemus", "pg_across_cycles": "Sinu tsüklite lõikes", "community_store": "Kogukonna pood", "online_account": "Kogukonna pood ja veebifunktsioonid", - "import_power_history": "Võimsuse ajaloo import" + "import_power_history": "Võimsuse ajaloo import", + "service_status": "Hoolduse olek" }, "health": { "fair": "Rahuldav profiili kvaliteet", @@ -373,6 +386,10 @@ "poor": "⚠ Kehv profiili kvaliteet" }, "lbl": { + "envelope_position_tip": "Asukoht sobiva programmi enda salvestatud kõveral, {pct}%. Määratakse selle tsükli kuju võrdlemisel profiiliga, mitte aja lugemisel, seega püsib õige ka siis, kui tsükkel on tavalisest pikem või lühem. See värskendub, kui seade on vaikne, seega aktiivse faasi ajal võib maha jääda.", + "envelope_position": "kõver {pct}%", + "aria_playground_chart3": "Interaktiivne tsükli võimsusgraafik: hõljuta aja/võimsuse lugemiseks, keri või näpista suumimiseks, lohista liigutamiseks", + "device_unresolved": "Seade pole saadaval ({id})", "drag_to_resize": "Lohistage suuruse muutmiseks", "pg_anti_wrinkle": "Kortsudevastane", "actions": "Tegevused", @@ -508,7 +525,7 @@ "profile_index": "profiili indeks →", "profile_name": "Profiili nimi", "program": "Programm", - "program_tip": "Asendage profiil, mis sobitatakse praeguse tsükliga. Automaatne tuvastamine laseb integratsioonil automaatselt valida parima vaste. Kinnitage konkreetne programm sundvastendamiseks, kui automaatne tuvastamine on vale või teate, mis töötab.", + "program_tip": "Asendage profiil, mis sobitatakse praeguse tsükliga. Automaatne tuvastamine laseb integratsioonil automaatselt valida parima vaste. Kinnitage konkreetne programm sundvastendamiseks, kui automaatne tuvastamine on vale või teate, mis töötab. Valige programm enne seadme käivitamist ja see rakendatakse kohe, kui algab järgmine tsükkel.", "progress": "Edusammud", "raw_socket": "Toores signaal", "read_only": "Kirjutuskaitstud", @@ -543,6 +560,7 @@ "stage4": "Etapp 4 – kokkulangevus", "status": "Olek", "tail_trim": "Kärbe lõpust (s)", + "tap_to_dismiss": "puuduta graafikut sulgemiseks", "timer_auto_pause": "Automaatne paus", "timer_min": "min", "timer_msg_placeholder": "Sõnum (valikuline, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Aega jäänud (mudel)", "pg_time_left_model_tip": "Mudeli hinnatud järelejäänud aeg (faasi hindaja + ML-segu), mitte staatiline loendur", "phase": "Faas", - "pg_mode_simulate": "Simuleeri", "pg_mode_history": "Testi ajalooga", - "pg_mode_sweep": "Pühkimine", "pg_ev_detected": "Tuvastatud", "pg_ev_match": "Vaste kinnitatud", "pg_ev_match_changed": "Vaste muutus", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Defineeri automaatselt tuvastatud ooterežiimi põhi käsitsi (jäta tühjaks automaatse jaoks).", "pg_stress_toggle": "Testi tühikäigu lõpetamist", "pg_stress_toggle_desc": "Simuleerib seadet, mis jääb salvestuse lõpus ooterežiimi tarbimise tasemele - näitab, kas ja millal WashData tsükli peatab.", - "pg_sweep_2d": "2D soojuskaart", - "pg_sweep_param_y": "2. parameeter", "pg_obj_match": "Vaste täpsus", "pg_obj_endtiming": "Lõpuaja täpsus", "pg_obj_falseend": "Valelõppude määr", "pg_obj_overrun": "Kestuse kõrvalekalle", "pg_obj_ambiguity": "Ebaselguse määr", - "pg_best_combo": "parim kombinatsioon", "pg_mode_optimize": "Optimeeri", "pg_group_matching": "Programmi sobitamine", "pg_zoom_hint": "topeltklõps suumi lähtestamiseks", @@ -706,7 +719,12 @@ "task_history_import_apply": "Tsüklite importimine", "evidence_real_cycles": "Selle seadme tehtud tsüklid", "evidence_reference_cycles": "Kogukonna poest alla laaditud", - "evidence_backfill_cycles": "Leitud imporditud võimsuse ajaloos" + "evidence_backfill_cycles": "Leitud imporditud võimsuse ajaloos", + "zoom_hint_touch": "näpista suumimiseks", + "zoom_hint_touch_zoomed": "näpista suumimiseks · topeltpuudutus lähtestamiseks", + "cycles_since_service": "{since} / {total} tsüklit", + "total_cycles_run": "tsüklit kokku", + "correct_total_cycles": "Paranda koguarv" }, "log": { "all_levels": "Kõik tasemed", @@ -756,7 +774,7 @@ "merge_cycles": "Ühendage {n} tsüklit", "new_group": "Uus profiilirühm", "new_phase": "Uus faas", - "process_history_msg": "Käivitage sobitamine uuesti, värskendage soovitusi, treenige ML uuesti (kui lubatud) ja arvutage tsükli tervis ümber kõigi salvestatud tsüklite jaoks. See võib võtta aega.", + "process_history_msg": "Käivitage kõigi salvestatud tsüklite jaoks sobitamine uuesti, värskendage soovitusi, treenige ML uuesti (kui lubatud), arvutage maksumus salvestatud energiahindade järgi ümber ja arvutage tsükli tervis ümber. See võib võtta aega.", "process_history_ok": "Töötle", "process_history_title": "Töötle ajalugu", "process_recording": "Protsessi salvestamine", @@ -789,6 +807,7 @@ "history_import": "Võimsuse ajaloo import" }, "msg": { + "pg_canvas_empty3": "Vali ülalt tsükkel ja vajuta Käivita, et seda simuleerida. Seejärel hõljuta väärtuste lugemiseks, keri või näpista suumimiseks ja lohista liigutamiseks.", "tail_trim_hint": "Mitu sekundit lõpust eemaldada", "store_sibling_hint": "Sinu täpse mudeli jaoks pole midagi jagatud? Sama brändi lähedane mudel on tavaliselt hea lähtepunkt.", "store_declare_appliance": "Ütle WashDatale, milline seade sul on, ja see vahekaart näitab teiste jagatud programme ja salvestisi selle seadme jaoks. Võid ka ülal brändi kirjutada ja lihtsalt ringi vaadata.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Ületas tavapärase võimsusriba umbes {n}s jooksul.", "auto_label_intro": "Määrake profiilid märgistamata tsüklitele, mille sobivuse usaldus eemaldab läve.", "automations_intro": "WashData käivitab {start} / {end} sündmusi ja avaldab olemeid, seega on teated ja toimingud kõige paremini ehitada tavaliste Home Assistanti automaatikana. Seda seadet kasutavad automaatikad kuvatakse allpool.", - "cleanup_intro": "Iga märgistatud tsükkel on kaetud. Profiili puhastamiseks märkige kõrvalekalded ja kustutage.", + "cleanup_intro": "Kõik märgistatud tsüklid on kohakuti kuvatud. Märgi kõrvalekalded ja eemalda nende märgistus (need jäävad ajalukku, kuid ei kujunda enam seda profiili) või kustuta need täielikult.", "clear_debug_hint": "Ruumi vabastamiseks eemaldage salvestatud silumisandmed.", "collecting_data": "Andmete kogumine. Tsükleid on enne peenhäälestuse algust veel vaja: {need} ({current}/{min}).", "compare_overlay_profiles": "Ülekatteprofiilid (nõrkjad)", @@ -901,7 +920,7 @@ "preferences_admin": "Eelistused, paneel ja juurdepääsu kontroll", "preferences_adv": "Eelistused", "prefs_personal": "Need kehtivad ainult teie koduabilise konto kohta.", - "process_history_hint": "Käivitage sobitamine uuesti kõigi salvestatud tsüklite jaoks, värskendage häälestamise soovitusi, treenige ML-mudeleid uuesti (kui lubatud) ja arvutage tsükli tervis ümber. Käivitage see pärast ülevaatuste partiid.", + "process_history_hint": "Käivitage sobitamine uuesti kõigi salvestatud tsüklite jaoks, värskendage häälestamise soovitusi, treenige ML-mudeleid uuesti (kui lubatud), arvutage tsüklite maksumus salvestatud energiahindade järgi ümber ja arvutage tsükli tervis ümber. Käivitage see pärast hulga ülevaatuste tegemist.", "profile_deleted": "Profiil kustutatud", "profile_poor_health_detail": "Sellele profiilile määratud tsüklitel on ebaühtlane kuju või madal usaldusväärsus. Kaaluge ümbriku ümberehitamist või märgistatud tsüklite ülevaatamist.", "profiles_intro": "Statistika, faaside ja puhastamise vaatamiseks klõpsake profiilil. Grupeerige peaaegu identsed programmid (sama kuju/kestus, erinev temperatuur või tsentrifuugimine), nii et sobitamine valib nende vahel usaldusväärselt.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' profiilil on madal sobivusskoor - selle hiljutised tsüklid varieeruvad palju või sobituvad nõrgalt. Vaata selle tsüklid üle või salvesta profiil uuesti, et tuvastus ja ajahinnangud püsiksid täpsed.", "advisory_shape_drift": "'{name}' on oma algsest võimsuskõverast märgatavalt kõrvale kaldunud. Seadme käitumine võib olla aja jooksul muutunud (nt katlakivi, kulumine). Kaalu selle profiili uuesti salvestamist hiljutiste tsüklitega.", "advisory_shape_drift_corr": "'{name}' on oma algsest võimsuskõverast märgatavalt kõrvale kaldunud (korrelatsioon {corr}). Seadme käitumine võib olla aja jooksul muutunud (nt katlakivi, kulumine). Kaalu selle profiili uuesti salvestamist hiljutiste tsüklitega.", + "advisory_unmatchable": "'{name}' ei saa kunagi sobituda: selle taga pole ühtegi võimsusandmetega tsüklit, seega pole WashDatal käimasolevat tsüklit millegagi võrrelda. Märgista mõni tsükkel selle programmiga (või salvesta üks), et profiil sobitamisse tagasi tuua.", + "advisory_unmatchable_no_duration": "'{name}' ei saa kunagi sobituda: sellel on võimsusandmetega tsükkel, kuid kasutatav kestus puudub, seega pole WashDatal pikkust, mille järgi sobitust mõõta. Salvesta see programm uuesti või määra selle eeldatav kestus, et profiil sobitamisse tagasi tuua.", "pg_no_alerts": "Selles käivituses ei tuvastatud probleeme.", "pg_override_active": "Kasutatakse su muudetud seadeid vs praegused", - "pg_history_intro": "Esita oma hiljutised tsüklid uuesti läbi päris tuvastaja ja sobitaja. Muuda esmalt tuvastuse seadet režiimis Simuleeri, et näha siin enne/pärast võrdlust.", "pg_history_empty": "Vajuta Käivita, et oma tsüklid uuesti esitada.", "pg_history_summary": "{detected}/{total} tuvastatud · {correct} õigesti sobitatud", "pg_sweep_no_metric": "Selle eesmärgi hindamiseks pole piisavalt andmeid.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Skannimine ebaõnnestus.", "hist_scan_expired": "See skannimistulemus pole enam saadaval. Palun skanni uuesti.", "hist_import_failed": "Import ebaõnnestus.", - "imported_history_readonly": "Tuvastatud imporditud võimsuse ajaloos. See mõjutab programmide sobitamist, kuid ei arvestata sinu statistikas ning seda ei saa kärpida ega poolitada. Kasuta nuppu Sildista, et määrata programm." + "imported_history_readonly": "Tuvastatud imporditud võimsuse ajaloos. See mõjutab programmide sobitamist, kuid ei arvestata sinu statistikas ning seda ei saa kärpida ega poolitada. Kasuta nuppu Sildista, et määrata programm.", + "odometer_intro": "See koguarv saab ainult kasvada. Tsüklikirje kustutamine seda ei muuda ja loendamine jätkub ka üle salvestatud ajaloo piiri, nii et hoolduse meeldetuletused jäävad õigeks.", + "odometer_correct_hint": "Määra see, kui WashData salvestas käituse, mida kunagi ei toimunud, või kui seade oli tsükleid läbinud juba enne WashData paigaldamist.", + "no_reminders_set": "Hoolduse meeldetuletusi pole veel määratud.", + "toast_program_armed": "Programm seatud järgmise tsükli jaoks: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Nõudepesumasin: vaiksed sekundid pärast eeldatavat kestust, enne kui tsükli lõpu tühjenduse ootamine vabastatakse", @@ -1073,6 +1097,8 @@ "anti_wrinkle_idle_timeout": "Kahe trummelimpulsi vahel lubatud vaikuseaeg enne kortsudevastase režiimi lõppu", "anti_wrinkle_max_duration": "Sellest pikem impulss lõpetab kortsudevastase režiimi ja avab uue tsükli", "anti_wrinkle_max_power": "Sellest kõrgem impulss lõpetab kortsudevastase režiimi ja avab uue tsükli", + "anti_crease_finalize_ratio": "Osa vastendatud programmi eeldatavast kestusest, milleni tsükkel peab jõudma, enne kui kortsudevastase trummeldamise saba tohib lõpetada; vähendage seda pika sabaga kuivatitel, jätke pesumasinad vaikeväärtusele", + "curve_preroll_seconds": "Katkenud käivituskatsete näitude sekundid, mis kantakse tsükli kõvera algusse; 0 = väljas", "completion_min_seconds": "Lühim käivitus, mis loetakse päris tsükliks", "end_repeat_count": "Madalate näitude arv järjest enne lõpetamist", "interrupted_min_seconds": "Lühikesed tsüklid märgitakse katkestatuks", @@ -1191,6 +1217,10 @@ "label": "Passiivkuivatuse vaikne vabastamine", "doc": "Kui tsükkel ületab oma eeldatava kestuse, siis kui kaua peab nõudepesumasin püsima vaikne (allpool Peatumise Lävi), enne kui WashData lõpetab lõpliku tühjenduse ootamise ja lõpetab tsükli. Suurendage seda, kui teie masinal on pikk vaikne kuivatusfaas enne hilist lõplikku tühjendust, mis jääb märkamata - laiem aken laseb õpitud kestusel järgida hooajalist triivi (külmem sissevoolav vesi = pikemad tsüklid), selle asemel et lukustuda vana keskmise külge. See saab ooteaega üksnes lühendada võrreldes sisemise 30-minutilise lõpupiigi ülempiiriga, mitte kunagi seda pikendada." }, + "anti_crease_finalize_ratio": { + "label": "Kortsudevastase lõpetamise suhe", + "doc": "Kui kaugele vastendatud programmi eeldatavast kestusest peab tsükkel jõudma, enne kui WashData tohib otsustada, et nähtav väikese võimsusega trummeldamine on kortsudevastane saba, ja tsükli seal lõpetada. Eeldatav kestus on programmi keskmine, seega kuivatil, mille andurkuivatuse tööaeg sõltub pesukogusest, ei jõua enamik tsükleid kunagi 98%-ni sellest ning saba jääb selle asemel ootama tagavara-aegumist - sageli 15 kuni 40 minutit pärast seda, kui masin tegelikult seiskus. Vähendage seda (nt 0,75) sellisel kuivatil. Jätke pesumasinad vaikeväärtusele: pesumasin püsib suurema osa oma tsüklist allpool seadet Maks Kortsudevastane Võimsus, nii et see osakaal on ainus, mis eristab pesu keskel olevat vaikset faasi sabast, ja selle vähendamine võib pesu poole lühemaks lõigata. Jätke tühjaks vaikeväärtuse jaoks (0,98). See on eri seade kui Nutika lõpetamise suhe, mis juhib eraldi varajase lõpetamise kontrolli." + }, "anti_wrinkle_enabled": { "doc": "Tuvastage lühikesed väikese võimsusega trummelimpulsid, mida kuivati pärast peamist kuumutusfaasi kiirgab, ja hoidke need valmis tsükliga ühendatud, selle asemel, et lugeda neid uute tsüklitena.", "label": "Luba Kortsudevastane Tuvastamine" @@ -1223,6 +1253,10 @@ "doc": "Sellest lühemad tsüklid jäetakse kummitustsüklitena kõrvale (testisõidud, soki lisamiseks ukse avamine).", "label": "Min Tsükli Kestus" }, + "curve_preroll_seconds": { + "label": "Kõvera eeljooks", + "doc": "Mitu sekundit katkenud käivituskatsete näite tohib WashData kanda tsükli kõvera algusse. Tsükli kõver algab tavaliselt sellest käivituskatsest, mis lõpuks õnnestub, seega masinal, mis kobab paar korda enne tegelikku käivitumist - programmi valik, ukselukk, esimene veevõtt - puuduvad igalt kõveralt tegeliku tegevuse esimesed minutid. Seadke see veidi suuremaks kui teie masina pikim selline eeljooks (300 s on enamikule küllaga). 0 lülitab selle välja. Pange tähele, et sisselülitamine nihutab salvestatud alguse varasemaks, nii et enne ja pärast muudatust salvestatud tsüklid kannavad sama programmi kohta erinevat kestust; õpitud keskmised triivivad, kuni vanemad tsüklid välja langevad." + }, "delay_confirm_seconds": { "doc": "Toide peab jääma ooterežiimi sagedusalasse nii kauaks, enne kui seadet käsitletakse kui käivitust ootavat, mitte töötavat.", "label": "Kinnitussekundid" @@ -1260,7 +1294,7 @@ "label": "Lõpu Kordusloendur" }, "energy_price_entity": { - "doc": "Andur praeguse elektrihinnaga kWh kohta (nt dünaamiline tariif). See on ülimuslik alloleva staatilise hinna suhtes. Iga tsükkel külmutab selle lõppedes kehtiva hinna.", + "doc": "Andur praeguse elektrihinnaga kWh kohta (nt dünaamiline tariif). See peab olema hind, mitte pistiku enda võimsuse või energia olem: see on ülimuslik alloleva staatilise hinna suhtes, seega siia määratud kWh loendur arvestab iga tsükli maksumuse arvesti näidu, mitte teie tariifi järgi. Kui Ajaliselt Kaalutud Maksumus on sees, arvestatakse iga tsükkel hinna järgi, mis kehtis igal selle töötamise hetkel; muidu fikseeritakse sellele hind, mis kehtis lõpetamise hetkel.", "label": "Energia Hinna Olem" }, "energy_price_static": { @@ -1356,7 +1390,7 @@ "label": "Käivita HA Sündmused Teavituste Jaoks" }, "notify_icon": { - "doc": "Märguande valikuline mdi-ikoon (nt mdi:pesumasin). Platvormi vaikeseadeks jätke tühjaks.", + "doc": "Teavitusel kuvatav väike ikoon (nt mdi:washing-machine). Android joonistab selle olekuribale; iOS kuvab selle rakenduse ikooni asemel ja vajab kaasrakendust 2026.8 või uuemat. Platvormi vaikeseade jaoks jätke tühjaks.", "label": "Teavituse Ikoon" }, "notify_live_chronometer": { @@ -1467,10 +1501,6 @@ "doc": "Mõni sekund võib pump pidevalt töötada, enne kui see märgitakse võimalikuks ummikuks (käivitab pumba kinnijäämise sündmuse).", "label": "Pumba Kinnijäämise Kestus" }, - "running_dead_zone": { - "doc": "Pärast tsükli algust eiratakse selle akna võimsuse langusi. Pesumasinad täituvad külma veega (enne kuumutamist langeb 0 W lähedale) – ilma selle kaitseta näeb täitmisfaas välja nagu tsükli lõpp. See EI jäta andmeid vahele: täisvõimsuse jälg salvestatakse alates T=0. Soovitusmootor mõõdab teie masina tegelikku käivitusmustrit ja määrab selle suuruse automaatselt.", - "label": "Töötava Surnud Tsoon" - }, "sampling_interval": { "doc": "Eeldatav aeg andurite näitude vahel – kasutatakse silumisakna suuruse määramiseks ja tagasilöögi õigeks alustamiseks. Iga anduri värskendus salvestatakse olenemata sellest väärtusest; see kalibreerib ainult allavoolu arvutusi. Soovitusmootor mõõdab teie anduri tegelikku sagedust eelmiste tsüklite põhjal ja määrab selle automaatselt.", "label": "Proovivõtu Intervall" @@ -1598,13 +1628,17 @@ "label": "Võimsusprofiilivahemik", "doc": "Ämbri suurus profiili power_profile sensori atribuudile (tasane pesa kohta keskmiste vattide massiiv, mida kasutavad välised planeerijad nagu EMHASS ja tibber_prices). Väiksemad ämbrid hoiavad lühikesi võimsushüppeid teravana; suuremad ämbrid siluvad kuju. Vaikimisi 15 min. Ainult lugemisaeg; ei mõjuta tuvastust." }, + "notify_live_silent": { + "label": "Vaiksed reaalajas uuendused", + "doc": "Ainult iOS. Värskenda reaalajas edenemist vaikselt: iga uuendus saabub helita ja vibratsioonita, madalama, akut säästva prioriteediga, mida iOS võib korraks kokku koguda. Tsükli esimene uuendus (see, mis käivitab Live Activity) ning algus- ja lõputeavitused annavad endiselt tavapäraselt märku. Lülita see välja, kui soovid märguannet iga uuenduse kohta." + }, "notify_live_sticky": { "label": "Hoia reaalajas teavitus puudutuse järel", "doc": "Ainult Android. Tee reaalajas edenemise teavitus püsivaks (kleepuvaks), nii et selle puudutamine ei sulge käimasolevat lõime. Väljas hoiab vaikekäitumist, kus puudutamine teavituse sulgeb." }, "notify_live_click_action": { - "label": "Reaalajas teavituse puudutamise sihtkoht", - "doc": "Ainult Android. Kuhu reaalajas edenemise teavituse puudutamine avab (nt /lovelace/laundry või täielik URL) rakenduse avalehe asemel. Vaikimisi jaoks jätke tühjaks." + "label": "Teavituse puudutamise sihtkoht", + "doc": "Kuhu viib selle seadme teavituse puudutamine, nii Androidis kui ka iOS-is. Tühjaks jätmine avab selle seadme WashData paneeli. Mujale suunamiseks sisestage töölaua tee (nt /lovelace/laundry) või täielik URL, või \"none\", kui puudutamisel ei tohi midagi avaneda." }, "door_opens_at_end": { "label": "Uks avaneb automaatselt lõpus", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Programmi kujundavad tsüklid", "doc": "Milliseid tsükleid kasutatakse iga programmi võimsuskõvera koostamiseks ja lõpetatud tsükli sellega sobitamiseks. Kui mõne liigi märke eemaldad, ei kujunda see enam sinu programme, kuid midagi ei kustutata - tsüklid jäävad Tsüklite loendisse ning neid saab endiselt sildistada või eemaldada. Kasulik, kui sa imporditud andmeid ei usalda. Statistikat see ei mõjuta: seal arvestatakse alati ainult neid tsükleid, mida see seade tegelikult käivitas. Kõigi märkide eemaldamist eiratakse, sest ilma ühegi tsüklita programm ei saaks kunagi sobituda." + }, + "energy_price_dynamic": { + "label": "Ajaliselt Kaalutud Maksumus", + "doc": "Arvestab iga tsükli maksumuse hinna järgi, mis kehtis igal selle töötamise hetkel, mitte ühe hinna järgi, mis kehtis lõpetamisel. Toimib ainult hinna olemiga (staatiline hind ei saa muutuda). Lisaseadistust pole vaja: tsükli töö ajal jälgitakse hinda reaalajas ning Diagnostika all olev toiming Töötle ajalugu arvutab vanemate tsüklite maksumuse recorderi andmetest ümber. Väljas = klassikaline käitumine, üks lõpus fikseeritud hind." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Lõhestamine…", "deleting": "Kustutamine…", "imported": "Imporditud", - "preparing": "Ettevalmistamine…" + "preparing": "Ettevalmistamine…", + "unlabelling": "Märgistuse eemaldamine…" }, "tab": { "advanced": "Täpsemalt", @@ -1808,6 +1847,7 @@ "recording_stopped": "Salvestamine peatus", "review_saved": "Arvustus salvestatud", "settings_conflicts": "Parandage esiletõstetud sätete konfliktid enne salvestamist.", + "settings_no_changes": "Pole muudatusi, mida salvestada", "settings_reverted": "Seaded taastatud; integratsiooni uuesti laadimine", "settings_saved": "Seaded salvestatud; integratsiooni uuesti laadimine", "suggestions_applied": "Soovitused rakendatud; integratsiooni uuesti laadimine", @@ -1866,7 +1906,11 @@ "export_selective_done": "Eksport alla laaditud", "import_selective_done": "Imporditud {profiles} profiili ja {cycles} tsüklit", "hist_csv_required": "Laadi esmalt CSV-fail või kleebi selle sisu", - "file_read_failed": "Seda faili ei õnnestunud lugeda" + "file_read_failed": "Seda faili ei õnnestunud lugeda", + "unlabel_done": "{count} tsükli märgistus eemaldatud", + "unlabel_failed": "Märgistuse eemaldamine ebaõnnestus: {error}", + "odometer_saved": "Tsüklite koguarv salvestatud", + "odometer_save_failed": "Koguarvu ei õnnestunud salvestada: {error}" }, "suggestion": { "both_agree": "WashData soovitab", @@ -1913,10 +1957,6 @@ "higher": "Kinnitab ainult suure kindlusega vasteid; rohkem tsükleid jääb märgistamata", "lower": "Sobib rohkema tsükliga; mõnda võib olla veidi vale" }, - "running_dead_zone": { - "higher": "Ignoreerib lühikesi voolutõuse jõudeolekus – vähem müra suhtes tundlik", - "lower": "Reageerib lühematele energiapursetele – tabab kiiret mööduvat aktiivsust" - }, "start_threshold_w": { "higher": "Väldib lühikestest energiatõusudest tulenevaid valesid käivitusi", "lower": "Tabab madalvõimsusega programmid; võib käivituda müra peale" @@ -1941,6 +1981,8 @@ "end_repeat_count": "{false}/{total} puhtast tsüklist ({pct}%) sisaldas üle 60s kestnud sisemist pausi, mis jätkus; nõuab {val} lõpukinnitust.", "learning_confidence": "p05 usaldusväärsus {cycles} kasutaja märgistatud tsükli puhul ({p05}); sellest allpool küsitakse kinnitust.", "match_interval": "Põhineb täheldatud uuendustempol (mediaan={median}s) * 10.", + "match_interval_capped": "Piiratud nii, et {persistence} järjestikust vastet mahuks lühima programmi ({shortest}s) {pct}% sisse; ainuüksi uuendustempo (mediaan={median}s) oleks soovitanud pikemat intervalli.", + "match_interval_floored": "Ainuüksi lühim programm ({shortest}s) lubaks siin kõige rohkem {cap}s, mis jääb alla {minimum}s miinimumi, seega hoitakse väärtust miinimumis: {persistence} järjestikust vastet võtab {budget}s, mis on rohkem kui {pct}% sellest programmist. Sobitamine käivitub ainult siis, kui saabub uus näit (mediaan={median}s), seega miinimum ei maksa midagi.", "max_duration_ratio": "Põhineb märgistatud tsüklite kestustel (p95={p95}).", "min_active": "Põhineb viimases tsüklis täheldatud madalaimal aktiivvõimsusel ({min}W).", "min_duration_ratio": "Hoitakse võimalikult madal, et programm tuvastataks tsükli alguses varakult; usaldus- ja mitmetimõistetavuse lävid hoiavad ära enneaegsed otsused.", @@ -1998,10 +2040,11 @@ "date_tip": "Kuupäev ja kellaaeg, mil tsükkel algas.", "duration_tip": "Tsükli kogukestus algusest lõpuni.", "energy_tip": "Kokku tarbitud energia (kWh). Arvutatud võimsuse integreerimisel aja jooksul.", - "cost_tip": "Selle tsükli energiakulu, fikseeritud lõpetamisel tollal kehtinud hinna järgi (energia x hind kWh kohta). Selle täitmiseks määra hind seadetes.", + "cost_tip": "Selle tsükli energiakulu, fikseeritud lõpetamisel. Kui Ajaliselt Kaalutud Maksumus on sees, arvestatakse võimsuskõver hinna järgi, mis kehtis igal hetkel; muidu energia x üks hind, mis kehtis lõpus. Selle täitmiseks määra hind seadetes.", "confidence_tip": "Profiili vaste usaldus (0-100%). Kui täpselt tsükli võimsuskõver vastas tuvastatud programmile.", "health_tip": "ML tsükli tervis (kõrgem = parem). Klõpsa tsüklil, et seda uurida ja üle vaadata.", - "flags_tip": "Tsükli ülevaatus-, anomaalia- ja allika embleemid. Hõljutage ikooni kohal üksikasja nägemiseks." + "flags_tip": "Tsükli ülevaatus-, anomaalia- ja allika embleemid. Hõljutage ikooni kohal üksikasja nägemiseks.", + "cost_dynamic_tip": "Ajaliselt kaalutud: arvestatud tsükli ajal kehtinud hinna järgi (tegelik {price} {cur}/kWh)." }, "stat": { "avg": "keskm.", @@ -2028,9 +2071,9 @@ "finished": "Tsükkel jõudis lõppolekusse ja lõppes." }, "store": { + "search_ph2": "Otsi brändi või mudeli järgi…", "your_model_tip": "See on seade, mille sa seadetes määrasid", "your_model": "Sinu oma", - "search_brand_ph": "Otsi brändi järgi…", "programs_count": "Programmid: {n}", "browse": "Sirvi", "device": "Seade", @@ -2075,7 +2118,8 @@ "golden": "Uuesti töötlemine: võrdlustsüklite märkimine", "suggestions": "Uuesti töötlemine: soovitused", "ml_training": "Uuesti töötlemine: ML-i treenimine", - "health": "Uuesti töötlemine: tsüklite tervis" + "health": "Uuesti töötlemine: tsüklite tervis", + "costs": "Uuesti töötlemine: energiakulud" }, "pg_sweep": { "optimize": "Optimeerimine: {param}" diff --git a/custom_components/ha_washdata/translations/panel/fi.json b/custom_components/ha_washdata/translations/panel/fi.json index 6f2c9aea..3db60638 100644 --- a/custom_components/ha_washdata/translations/panel/fi.json +++ b/custom_components/ha_washdata/translations/panel/fi.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Loppua kohti tämä ohjelma on hiljaa noin {mins} min, minkä jälkeen se ottaa noin {watts} W {secs} s ajan ennen valmistumista. Havaittu {seen}/{measured} mitatussa syklissä: laite ei tee näin joka ajolla.", + "quiet_tail": "~{mins} min hiljainen loppu", "artifact_tip": "{n} poikkeavaa havaittu (esim. oven avautuminen kesken syklin) – avaa nähdäksesi ne kaaviossa", "auto": "(tunnistettu automaattisesti)", "built_in_tag": "sisäänrakennettu", @@ -63,7 +65,7 @@ "needs_review": "Vaatii tarkistuksen", "overrun": "Juoksi tavallista pidempään", "poor_fit": "heikko laatu", - "poor_fit_tip": "Epäjohdonmukainen otteluhistoria – harkitse tämän profiilin rakentamista uudelleen", + "poor_fit_tip": "Epäjohdonmukainen täsmäytyshistoria – harkitse tämän profiilin rakentamista uudelleen", "restart_gap_tip": "{n} HA-uudelleenkäynnistyskatkos: tehoviivan datassa on aukko", "reviewed": "Tarkistettu", "steady": "→ tasainen", @@ -77,11 +79,14 @@ "approved": "Hyväksytty", "awaiting": "Odottaa hyväksyntää", "imported_tip": "Tuotu yhteisökaupasta. Käytetään vain täsmäytykseen, ei lasketa tilastoihin.", + "unmatchable": "ei täsmäytettävissä", "not_importable": "ei käytettävissä", "exists": "on jo", - "backfilled_tip": "Tunnistettu tuodusta tehohistoriasta. Vaikuttaa vain ohjelmien täsmäytykseen, ei lasketa tilastoihin." + "backfilled_tip": "Tunnistettu tuodusta tehohistoriasta. Vaikuttaa vain ohjelmien täsmäytykseen, ei lasketa tilastoihin.", + "armed": "(koskee seuraavaa sykliä)" }, "btn": { + "reset_zoom": "Nollaa zoomaus", "set_brand_model": "Aseta merkki ja malli", "refresh_catalog": "Päivitä luettelo", "add_device": "+ Lisää laite", @@ -215,6 +220,8 @@ "trim": "Leikkaa", "trim_split": "Leikkaa / jaa", "undo": "Kumoa", + "unlabel_selected": "Poista valittujen merkintä ({n})", + "unlabel_selected_tip": "Poistaa profiilimerkinnän valituista sykleistä. Ne säilyvät historiassasi ja ne voidaan merkitä myöhemmin uudelleen - ne eivät vain enää muovaa tätä profiilia.", "use": "Käytä", "wipe_all": "Pyyhi kaikki tiedot", "delete_selected": "Poista valitut ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Käytä parasta", "pg_apply_to_settings": "Tallenna asetuksiin", "pg_apply_to_settings_tip": "Kopioi tässä muokkaamasi arvot tämän laitteen aktiivisiin asetuksiin", - "search": "Hae", "import": "Tuo", "disconnect": "Katkaise yhteys", "connect_github": "Yhdistä GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Lue Home Assistantista", "hist_scan": "Etsi syklejä", "hist_import_n": "Tuo {n} sykliä", - "hist_goto_cycles": "Näytä syklit" + "hist_goto_cycles": "Näytä syklit", + "save_total_cycles": "Tallenna kokonaismäärä" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Täytyy olla yli virran sammutuksen kynnyksen ({pot} W)", "threshold": "Täytyy olla alle pysäytyskynnyksen ({stop} W), jotta se astuu voimaan" }, + "price_entity": { + "power_sensor": "Tämä on laitteen Tehoanturi, ei hinta. Hintaentiteetti ohittaa alla olevan kiinteän energiahinnan, joten sykleistä veloitettaisiin wattilukema kWh:n hintana - tyhjennä tämä kenttä.", + "energy_sensor": "Tämä on laitteen Energiamittari, ei hinta. Hintaentiteetti ohittaa alla olevan kiinteän energiahinnan, joten sykleistä veloitettaisiin mittarin lukema kWh:n hintana - tyhjennä tämä kenttä.", + "device_class": "Tämä anturi mittaa suuretta {dc}, ei hintaa per kWh. Hintaentiteetti ohittaa alla olevan kiinteän energiahinnan - tyhjennä tämä kenttä tai valitse tariffianturi.", + "unit": "Tämän anturin lukeman yksikkö on {unit}, joka ei ole hinta per kWh. Hintaentiteetti ohittaa alla olevan kiinteän energiahinnan - tyhjennä tämä kenttä tai valitse tariffianturi." + }, "pump_stuck": { "duration": "Täytyy olla alle päivityksettömyyden aikakatkaisun ({to} s), jotta jumihälytys aktivoituu ennen kuin tarkkailija lopettaa syklin", "timeout": "Täytyy ylittää pumpun jumiutumiskesto ({ps} s), jotta jumihälytys aktivoituu ennen syklin pakotettua pysäytystä" @@ -349,7 +362,7 @@ "pg_settings_source": "Asetusten lähde", "phase_catalog": "Vaiheen katalogi", "playground": "Testialue", - "process_history": "Prosessihistoria", + "process_history": "Käsittele historia", "settings_history": "Asetusten historia", "status": "Tila", "status_graph": "Tilakaavio", @@ -360,12 +373,12 @@ "wipe_history": "Pyyhi historia", "recommendations": "Suositukset ({n})", "pg_detection_params": "Tunnistusasetukset", - "pg_timeline": "Tapahtuma-aikajana", "pg_outcome": "Simulaation tulos", "pg_across_cycles": "Kaikissa jaksoissasi", "community_store": "Yhteisökauppa", "online_account": "Yhteisökauppa ja verkkotoiminnot", - "import_power_history": "Tuo tehohistoria" + "import_power_history": "Tuo tehohistoria", + "service_status": "Huollon tila" }, "health": { "fair": "Tyydyttävä profiililaatu", @@ -373,6 +386,10 @@ "poor": "⚠ Heikko profiililaatu" }, "lbl": { + "envelope_position_tip": "Sijainti täsmätyn ohjelman omalla tallennetulla käyrällä, {pct} %. Mitataan kohdistamalla tämä sykli profiiliin ajan laskemisen sijaan, joten se pysyy oikeana, vaikka ajo olisi tavallista pidempi tai lyhyempi. Se päivittyy laitteen ollessa hiljaa, joten se voi jäädä jälkeen aktiivisen vaiheen aikana.", + "envelope_position": "käyrä {pct} %", + "aria_playground_chart3": "Interaktiivinen jakson tehokaavio: vie osoitin päälle lukeaksesi ajan/tehon, vieritä tai nipistä zoomataksesi, vedä panoroidaksesi", + "device_unresolved": "Laite ei saatavilla ({id})", "drag_to_resize": "Vedä muuttaaksesi kokoa", "pg_anti_wrinkle": "Ryppyjenesto", "actions": "Toiminnot", @@ -508,7 +525,7 @@ "profile_index": "profiilin indeksi →", "profile_name": "Profiilin nimi", "program": "Ohjelma", - "program_tip": "Ohita, mikä profiili sovitetaan nykyiseen sykliin. Automaattinen tunnistus antaa integraation valita parhaan vastaavuuden automaattisesti. Kiinnitä tietty ohjelma pakottaaksesi vastaavuuden, kun automaattinen tunnistus on väärässä tai tiedät, mitä on käynnissä.", + "program_tip": "Ohita, mikä profiili sovitetaan nykyiseen sykliin. Automaattinen tunnistus antaa integraation valita parhaan vastaavuuden automaattisesti. Kiinnitä tietty ohjelma pakottaaksesi vastaavuuden, kun automaattinen tunnistus on väärässä tai tiedät, mitä on käynnissä. Valitse ohjelma ennen laitteen käynnistämistä, niin se otetaan käyttöön heti kun seuraava sykli alkaa.", "progress": "Edistyminen", "raw_socket": "Raaka signaali", "read_only": "Vain luku", @@ -534,7 +551,7 @@ "settings_basic": "Perus", "settings_detail_level": "Asetusten yksityiskohtien taso", "settings_overrides": "Asetusten ohitukset", - "show_debug": "Näytä live-ottelun virheenkorjauskortti Tila-sivulla (luottamus, epäselvyys, parhaat ehdokkaat)", + "show_debug": "Näytä reaaliaikaisen täsmäytyksen virheenkorjauskortti Tila-sivulla (luottamus, epäselvyys, parhaat ehdokkaat)", "show_expected": "Näytä odotettu käyrän peittokuva (vastaava profiili, oranssi)", "show_raw": "Näytä raakadatan pistorasiakytkin reaaliaikaisessa tehograafissa", "sparkline": "Viimeaikainen syklin keston suuntaus", @@ -543,6 +560,7 @@ "stage4": "Vaihe 4 – vastaavuus", "status": "Tila", "tail_trim": "Leikkaus lopusta (s)", + "tap_to_dismiss": "napauta kaaviota sulkeaksesi", "timer_auto_pause": "Automaattinen tauko", "timer_min": "min", "timer_msg_placeholder": "Viesti (valinnainen, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Aikaa jäljellä (malli)", "pg_time_left_model_tip": "Mallin arvioima jäljellä oleva aika (vaihe-estimaattori + ML-yhdistelmä), ei kiinteä laskuri", "phase": "Vaihe", - "pg_mode_simulate": "Simuloi", "pg_mode_history": "Testaa historialla", - "pg_mode_sweep": "Pyyhkäisy", "pg_ev_detected": "Havaittu", "pg_ev_match": "Osuma vahvistettu", "pg_ev_match_changed": "Osuma vaihtui", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Ohita automaattisesti havaittu valmiustilasyöte (jätä tyhjäksi automaattista varten).", "pg_stress_toggle": "Testaa valmiustilan päätöstä", "pg_stress_toggle_desc": "Simuloi laitetta, joka pysyy valmiustilan kulutustasollaan tallennuksen jälkeen - näyttää, pysäyttääkö WashData syklin ja milloin.", - "pg_sweep_2d": "2D-lämpökartta", - "pg_sweep_param_y": "2. parametri", "pg_obj_match": "Osumatarkkuus", "pg_obj_endtiming": "Lopetusajan tarkkuus", "pg_obj_falseend": "Väärien lopetusten osuus", "pg_obj_overrun": "Keston poikkeama", "pg_obj_ambiguity": "Epäselvyyksien osuus", - "pg_best_combo": "paras yhdistelmä", "pg_mode_optimize": "Optimoi", "pg_group_matching": "Ohjelman täsmäytys", "pg_zoom_hint": "kaksoisnapsauta nollataksesi zoomauksen", @@ -706,7 +719,12 @@ "task_history_import_apply": "Syklien tuonti", "evidence_real_cycles": "Tämän koneen ajamat syklit", "evidence_reference_cycles": "Ladattu yhteisökaupasta", - "evidence_backfill_cycles": "Löydetty tuodusta tehohistoriasta" + "evidence_backfill_cycles": "Löydetty tuodusta tehohistoriasta", + "zoom_hint_touch": "nipistä zoomataksesi", + "zoom_hint_touch_zoomed": "nipistä zoomataksesi · kaksoisnapauta nollataksesi", + "cycles_since_service": "{since} / {total} sykliä", + "total_cycles_run": "sykliä yhteensä", + "correct_total_cycles": "Korjaa kokonaismäärä" }, "log": { "all_levels": "Kaikki tasot", @@ -756,7 +774,7 @@ "merge_cycles": "Yhdistä {n} sykliä", "new_group": "Uusi profiiliryhmä", "new_phase": "Uusi vaihe", - "process_history_msg": "Suorita täsmäytys uudelleen, päivitä ehdotukset, kouluta ML uudelleen (jos käytössä) ja laske syklipistemäärät uudelleen kaikkien tallennettujen syklien osalta. Tämä voi kestää hetken.", + "process_history_msg": "Suorita täsmäytys uudelleen, päivitä ehdotukset, kouluta ML uudelleen (jos käytössä), laske kustannukset uudelleen tallennettujen energiahintojen mukaan ja laske syklien kunto uudelleen kaikille tallennetuille sykleille. Tämä voi kestää hetken.", "process_history_ok": "Käsittele", "process_history_title": "Käsittele historia", "process_recording": "Prosessin tallennus", @@ -789,6 +807,7 @@ "history_import": "Tuo tehohistoria" }, "msg": { + "pg_canvas_empty3": "Valitse jakso yltä ja paina Suorita simuloidaksesi sen. Vie sitten osoitin päälle lukeaksesi arvoja, vieritä tai nipistä zoomataksesi ja vedä panoroidaksesi.", "tail_trim_hint": "Kuinka monta sekuntia poistetaan lopusta", "store_sibling_hint": "Eikö juuri sinun mallillesi ole jaettu mitään? Saman merkin läheinen malli on yleensä hyvä lähtökohta.", "store_declare_appliance": "Kerro WashDatalle, mikä laite sinulla on, niin tämä välilehti näyttää muiden sille jakamat ohjelmat ja tallennukset. Voit myös kirjoittaa merkin nimen yläkenttään ja katsella ympärillesi.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Ylitti tavallisen tehon ylärajan noin {n} sekunnin ajan.", "auto_label_intro": "Määritä profiilit nimeämättömille sykleille, joiden osuvuusvarmuus tyhjentää kynnyksen.", "automations_intro": "WashData laukaisee {start} / {end}-tapahtumat ja tarjoaa entiteetit, joten ilmoitukset ja toiminnot kannattaa rakentaa tavallisina Home Assistant -automaatioina. Tätä laitetta käyttävät automaatiot näkyvät alla.", - "cleanup_intro": "Jokainen merkitty sykli peitetään. Tyhjennä profiili valitsemalla poikkeamat ja poistamalla ne.", + "cleanup_intro": "Kaikki merkityt syklit näkyvät päällekkäin. Valitse poikkeamat ja poista niiden merkintä (ne säilyvät historiassa, mutta eivät enää muovaa tätä profiilia) tai poista ne kokonaan.", "clear_debug_hint": "Poista tallennetut virheenkorjaustiedot vapauttaaksesi tilaa.", "collecting_data": "Kerätään tietoja. Syklejä tarvitaan vielä ennen hienosäädön aloittamista: {need} ({current}/{min}).", "compare_overlay_profiles": "Päällekkäiset profiilit (himmeä)", @@ -901,7 +920,7 @@ "preferences_admin": "Asetukset, paneeli ja kulunvalvonta", "preferences_adv": "Asetukset", "prefs_personal": "Nämä koskevat vain Home Assistant -tiliäsi.", - "process_history_hint": "Suorita täsmäytys uudelleen kaikille tallennetuille sykleille, päivitä virityssuositukset, kouluta ML-mallit uudelleen (jos käytössä) ja laske syklipistemäärät uudelleen. Suorita tämä arviointierän jälkeen.", + "process_history_hint": "Suorita täsmäytys uudelleen kaikille tallennetuille sykleille, päivitä virityssuositukset, kouluta ML-mallit uudelleen (jos käytössä), laske syklien kustannukset uudelleen tallennettujen energiahintojen mukaan ja laske syklien kunto uudelleen. Suorita tämä arviointierän jälkeen.", "profile_deleted": "Profiili poistettu", "profile_poor_health_detail": "Tälle profiilille määritetyillä sykleillä on epäjohdonmukainen muoto tai alhainen luotettavuus. Harkitse kirjekuoren rakentamista uudelleen tai merkittyjen syklien tarkistamista.", "profiles_intro": "Napsauta profiilia nähdäksesi tilastot, vaiheet ja puhdistus. Ryhmittele lähes identtiset ohjelmat (sama muoto/kesto, eri lämpötila tai linkous), jotta yhteensopivuus valitsee niiden välillä luotettavasti.", @@ -915,7 +934,7 @@ "review_confirm_help": "Varmista, tunnistettiinko tämä jakso oikein. Arvostelusi opettavat mallia koneessasi – mitä enemmän jaksoja vahvistat, sitä paremmat yhteensopivuus- ja kuntopisteet ovat. Nopea Hyvä/Paha riittää.", "review_in_settings": "Tarkista asetuksista", "review_notes_placeholder": "Huomautuksia (valinnainen)", - "review_notes_tip": "Vapaatekstiiset muistiinpanot omaksi viitteeksi. Ei käytössä otteluissa tai harjoituksissa.", + "review_notes_tip": "Vapaamuotoiset muistiinpanot omaan käyttöösi. Ei käytetä täsmäytyksessä eikä koulutuksessa.", "review_profile_tip": "Tämän syklin ohjelma on nimetty. Jos automaattisesti havaittu ohjelma oli väärä, korjaa se tässä - merkintä opettaa vastaavuuden tulevia jaksoja varten.", "review_quality_tip": "Kuinka puhdas tämä kierto on. Hyvä = tämän ohjelman oppikirjaesimerkki; Huono = havaittu, mutta meluisa tai epätyypillinen; Käyttökelvoton = väärin havaittu (yhdistetty, katkaistu tai harhaanjohtava). Ajaa terveyspisteitä ja mitkä syklit ovat sallittuja mallin kouluttamiseen.", "review_recorded_tip": "Merkitse tämä käsin valituksi referenssijaksoksi ohjelmaansa varten - sama rooli kuin manuaalisesti tallennetulla jaksolla. Vertailusyklit säilytetään aina, siemennä vastaava malli, eikä niitä koskaan pudoteta puhdistuksen yhteydessä. (Tämä on \"kultainen\" / tallennettu lippu; molemmat ovat sama asia.)", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}'-profiilin sopivuusarvo on matala - sen viimeaikaiset jaksot vaihtelevat paljon tai tunnistuvat heikosti. Käy läpi sen jaksot tai tallenna profiili uudelleen, jotta tunnistus ja aika-arviot pysyvät tarkkoina.", "advisory_shape_drift": "'{name}' on muuttunut merkittävästi alkuperäisestä tehokäyrästään. Laitteen toiminta on voinut muuttua ajan myötä (esim. kalkki, kuluminen). Harkitse profiilin tallentamista uudelleen tuoreilla jaksoilla.", "advisory_shape_drift_corr": "'{name}' on muuttunut merkittävästi alkuperäisestä tehokäyrästään (korrelaatio {corr}). Laitteen toiminta on voinut muuttua ajan myötä (esim. kalkki, kuluminen). Harkitse profiilin tallentamista uudelleen tuoreilla jaksoilla.", + "advisory_unmatchable": "'{name}'-profiilia ei voi koskaan täsmäyttää: sen taustalla ei ole yhtään jaksoa, jossa olisi tehotietoja, joten WashDatalla ei ole mitään, mihin verrata käynnissä olevaa jaksoa. Merkitse jokin tämän ohjelman jakso (tai tallenna uusi), jotta profiili palaa täsmäytykseen.", + "advisory_unmatchable_no_duration": "'{name}'-profiilia ei voi koskaan täsmäyttää: sillä on jakso, jossa on tehotietoja mutta ei käyttökelpoista kestoa, joten WashDatalla ei ole pituutta, johon täsmäytyksen voisi suhteuttaa. Tallenna tämä ohjelma uudelleen tai aseta sen odotettu kesto, jotta profiili palaa täsmäytykseen.", "pg_no_alerts": "Tässä ajossa ei havaittu ongelmia.", "pg_override_active": "Käytetään muokattuja asetuksiasi vs. nykyiset", - "pg_history_intro": "Toista viimeisimmät syklisi todellisen tunnistimen ja täsmääjän läpi. Muokkaa ensin tunnistusasetusta Simuloi-tilassa nähdäksesi tässä ennen/jälkeen-vertailun.", "pg_history_empty": "Paina Aja toistaaksesi syklisi.", "pg_history_summary": "{detected}/{total} havaittu · {correct} täsmätty oikein", "pg_sweep_no_metric": "Ei tarpeeksi dataa tämän tavoitteen pisteyttämiseen.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Läpikäynti epäonnistui.", "hist_scan_expired": "Kyseinen läpikäynti ei ole enää saatavilla. Tee läpikäynti uudelleen.", "hist_import_failed": "Tuonti epäonnistui.", - "imported_history_readonly": "Tunnistettu tuodusta tehohistoriasta. Se vaikuttaa ohjelmien täsmäytykseen, mutta ei näy tilastoissasi, eikä sitä voi rajata tai jakaa. Nimeä ohjelma Merkitse-toiminnolla." + "imported_history_readonly": "Tunnistettu tuodusta tehohistoriasta. Se vaikuttaa ohjelmien täsmäytykseen, mutta ei näy tilastoissasi, eikä sitä voi rajata tai jakaa. Nimeä ohjelma Merkitse-toiminnolla.", + "odometer_intro": "Tämä kokonaismäärä voi vain kasvaa. Syklitietueen poistaminen ei muuta sitä, ja laskenta jatkuu myös tallennetun historian rajan yli, jotta huoltomuistutukset pysyvät oikeina.", + "odometer_correct_hint": "Aseta tämä, jos WashData kirjasi ajon, jota ei koskaan tapahtunut, tai jos laitteella oli jo ajettu syklejä ennen WashDatan asennusta.", + "no_reminders_set": "Huoltomuistutuksia ei ole vielä asetettu.", + "toast_program_armed": "Ohjelma asetettu seuraavaa sykliä varten: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Astianpesukone: hiljaiset sekunnit odotetun keston jälkeen ennen kuin syklin lopun vedenpoiston odotus vapautetaan", @@ -1073,6 +1097,8 @@ "anti_wrinkle_idle_timeout": "Kahden rumpupulssin välillä sallittu hiljainen aika ennen kuin ryppyjenesto päättyy", "anti_wrinkle_max_duration": "Tätä pidempi pulssi päättää ryppyjeneston ja aloittaa uuden syklin", "anti_wrinkle_max_power": "Tätä suurempi pulssi päättää ryppyjeneston ja aloittaa uuden syklin", + "anti_crease_finalize_ratio": "Osuus täsmätyn ohjelman odotetusta kestosta, joka syklin on saavutettava ennen kuin ryppyjeneston rummutushäntä voidaan päättää; laske sitä kuivausrummuissa, joissa on pitkä häntä, ja jätä pesukoneet oletusarvoon", + "curve_preroll_seconds": "Keskeytyneiden käynnistysyritysten lukemien sekunnit, jotka liitetään syklin käyrän alkuun; 0 = pois", "completion_min_seconds": "Lyhin ajo, joka lasketaan oikeaksi sykliksi", "end_repeat_count": "Peräkkäisten matalien lukemien määrä ennen päättymistä", "interrupted_min_seconds": "Lyhyet syklit merkitään keskeytyneiksi", @@ -1191,6 +1217,10 @@ "label": "Passiivikuivauksen hiljainen vapautus", "doc": "Kun sykli ylittää odotetun kestonsa, kuinka kauan astianpesukoneen on pysyttävä hiljaisena (Pysäytyskynnys-arvon alapuolella) ennen kuin WashData lopettaa lopullisen vedenpoiston odottamisen ja päättää syklin. Nosta arvoa, jos koneessasi on pitkä hiljainen kuivausvaihe ennen myöhäistä lopullista vedenpoistoa, joka jää huomaamatta - laajempi ikkuna antaa opitun keston seurata kausivaihtelua (kylmempi tulovesi = pidemmät syklit) sen sijaan, että se lukittuisi vanhaan keskiarvoon. Se voi vain lyhentää odotusta suhteessa sisäiseen 30 minuutin loppupiikkirajaan, ei koskaan pidentää sitä." }, + "anti_crease_finalize_ratio": { + "label": "Ryppyjeneston lopetussuhde", + "doc": "Kuinka pitkälle täsmätyn ohjelman odotettuun kestoon syklin on edettävä, ennen kuin WashData saa päätellä, että havaittu vähätehoinen rummutus on ryppyjeneston loppuhäntä, ja lopettaa syklin siihen. Odotettu kesto on ohjelman keskiarvo, joten kuivausrummussa, jonka anturikuivauksen ajoaika seuraa pyykkimäärää, useimmat ajot eivät koskaan yllä 98 %:iin siitä, ja häntä jää odottamaan vara-aikakatkaisua - usein 15-40 minuuttia sen jälkeen, kun kone tosiasiassa pysähtyi. Laske tätä (esim. 0,75) tällaisessa kuivausrummussa. Jätä pesukoneet oletusarvoon: pesukone on suurimman osan syklistään Ryppyjeneston enimmäistehon alapuolella, joten tämä osuus on ainoa asia, joka erottaa pesun keskellä olevan hiljaisen vaiheen hännästä, ja sen laskeminen voi puolittaa pesun. Jätä tyhjäksi käyttääksesi oletusta (0,98). Tämä on eri asetus kuin Älykkään lopetuksen suhde, joka ohjaa erillistä ennenaikaisen lopetuksen tarkistusta." + }, "anti_wrinkle_enabled": { "doc": "Tunnista lyhyet vähätehoiset rumpupulssit, joita kuivausrumpu lähettää päälämpövaiheen jälkeen, ja pidä ne kiinni valmiissa jaksossa sen sijaan, että luet ne uusina jaksoina.", "label": "Ota ryppyjenesto käyttöön" @@ -1212,7 +1242,7 @@ "label": "Ryppyjeneston enimmäisteho" }, "auto_label_confidence": { - "doc": "Jos ottelun tulos syklin lopussa on tätä suurempi tai suurempi, ohjelma merkitään automaattisesti ilman vahvistuskehotetta. Nosta se vaatimaan suurempaa varmuutta ennen automaattista merkitsemistä; alenna sitä automatisoidaksesi enemmän. Toimii yhdessä sen alla olevan Learning Confidencen kanssa.", + "doc": "Jos täsmäytyspisteet syklin lopussa ovat tässä tai sen yli, ohjelma merkitään automaattisesti ilman vahvistuskehotetta. Nosta sitä, jos haluat vaatia suurempaa varmuutta ennen automaattista merkintää; laske sitä automatisoidaksesi enemmän. Toimii yhdessä alla olevan Oppimisen varmuus -asetuksen kanssa.", "label": "Automaattimerkinnän varmuus" }, "auto_maintenance": { @@ -1223,6 +1253,10 @@ "doc": "Tätä lyhyemmät syklit hylätään haamujaksoina (koeajot, oven avaaminen sukan lisäämiseksi).", "label": "Syklin vähimmäiskesto" }, + "curve_preroll_seconds": { + "label": "Käyrän esijakso", + "doc": "Kuinka monta sekuntia keskeytyneiden käynnistysyritysten lukemia WashData saa liittää syklin käyrän alkuun. Syklin käyrä alkaa normaalisti siitä käynnistysyrityksestä, joka lopulta onnistuu, joten koneessa, joka kokeilee muutaman kerran ennen kuin se todella lähtee käyntiin - ohjelman valinta, luukun lukitus, ensimmäinen vedenotto - jokaisesta käyrästä puuttuvat todellisen toiminnan ensimmäiset minuutit. Aseta tämä hieman suuremmaksi kuin koneesi pisin tällainen alkuvaihe (300 s riittää useimmille). 0 poistaa toiminnon käytöstä. Huomaa, että käyttöönotto siirtää tallennettua alkuhetkeä aikaisemmaksi, joten ennen muutosta ja sen jälkeen tallennetuilla sykleillä on saman ohjelman kohdalla eri kestot; opitut keskiarvot ajautuvat, kunnes vanhemmat syklit poistuvat." + }, "delay_confirm_seconds": { "doc": "Virran on pysyttävä valmiustilassa niin kauan, ennen kuin laitetta käsitellään käynnistystä odottavana eikä käynnissä.", "label": "Vahvistusaika sekunteina" @@ -1260,7 +1294,7 @@ "label": "Lopetuksen toistomäärä" }, "energy_price_entity": { - "doc": "Anturi nykyisellä sähkön hinnalla kWh (esim. dynaaminen tariffi). Ensisijainen alla olevaan staattiseen hintaan. Jokainen sykli jäädyttää hinnan, joka on voimassa sen päättyessä.", + "doc": "Anturi nykyisellä sähkön hinnalla kWh (esim. dynaaminen tariffi). Sen on oltava hinta, ei pistokkeen oma teho- tai energiaentiteetti: se on ensisijainen alla olevaan staattiseen hintaan nähden, joten tähän valittu kWh-laskuri veloittaa jokaisesta syklistä mittarin lukeman oman tariffisi sijaan. Kun Aikapainotettu kustannus on käytössä, kukin sykli veloitetaan sillä hinnalla, joka oli voimassa jokaisella sen käyntihetkellä; muuten siihen lukitaan hinta, joka oli voimassa sen päättyessä.", "label": "Energiahinnan entiteetti" }, "energy_price_static": { @@ -1288,7 +1322,7 @@ "label": "Käännä ulkoinen signaali (laukaise POIS)" }, "learning_confidence": { - "doc": "Jos osumapisteet jäävät tämän ja Auto-Label Confidence -toiminnon väliin, palauteilmoitus pyytää sinua vahvistamaan tunnistetun ohjelman. Tämän pistemäärän alapuolella ottelu on liian epävarma tullakseen pintaan. On säilytettävä Auto-Label Confidence -arvon alapuolella.", + "doc": "Jos täsmäytyspisteet jäävät tämän ja Automaattimerkinnän varmuuden väliin, palauteilmoitus pyytää sinua vahvistamaan tunnistetun ohjelman. Tämän pistemäärän alle jäävä täsmäytys on liian epävarma näytettäväksi. Arvo on pidettävä Automaattimerkinnän varmuutta pienempänä.", "label": "Oppimisen varmuus" }, "linked_device": { @@ -1356,7 +1390,7 @@ "label": "Käynnistä HA-tapahtumat ilmoituksille" }, "notify_icon": { - "doc": "Valinnainen mdi-kuvake ilmoitukselle (esim. mdi:washing-machine). Jätä tyhjäksi alustan oletuksena.", + "doc": "Ilmoituksessa näytettävä pieni kuvake (esim. mdi:washing-machine). Android piirtää sen tilapalkkiin; iOS näyttää sen sovelluksen kuvakkeen tilalla ja vaatii kumppanisovelluksen version 2026.8 tai uudemman. Jätä tyhjäksi alustan oletusta varten.", "label": "Ilmoituksen kuvake" }, "notify_live_chronometer": { @@ -1456,7 +1490,7 @@ "label": "Täsmäytyskynnys" }, "profile_unmatch_threshold": { - "doc": "Jos live-ottelu puolivälissä putoaa tämän pistemäärän alapuolelle, alustava tunnistaminen tyhjennetään. Pidä se hieman vastaavuuskynnyksen alapuolella, jotta lyhyt samankaltaisuus ei käännä näyttöä takaisin vertaansa vailla.", + "doc": "Jos reaaliaikainen, kesken syklin tehty täsmäytys putoaa tämän pistemäärän alle, alustava tunnistus tyhjennetään. Pidä se hieman Täsmäytyskynnystä pienempänä, jotta lyhyt notkahdus samankaltaisuudessa ei palauta näyttöä tilaan Ei osumaa.", "label": "Eritäsmäytyskynnys" }, "progress_reset_delay": { @@ -1467,10 +1501,6 @@ "doc": "Sekunteja pumppu voi käydä jatkuvasti ennen kuin se merkitään mahdollisesti jumiutuneeksi (käynnistää juuttunut pumppu -tapahtuman).", "label": "Pumpun jumiutumiskesto" }, - "running_dead_zone": { - "doc": "Jakson alkamisen jälkeen tämän ikkunan sisällä olevat virrankatkaisut ohitetaan. Pyykinpesukoneet täyttyvät kylmällä vedellä (lämpenee lähes 0 W ennen lämmitystä) - ilman tätä suojaa täyttövaihe näyttää syklin päättymiseltä. Tämä EI ohita tietoja: täysi tehojälki tallennetaan T=0:sta. Ehdotusmoottori mittaa koneesi todellisen käynnistyskuvion ja määrittää sen automaattisesti.", - "label": "Käynnissäolon kuolualue" - }, "sampling_interval": { "doc": "Odotettu aika anturin lukemien välillä - käytetään tasoitusikkunan mitoittamiseen ja oikeaan palautumiseen. Jokainen anturin päivitys tallennetaan tästä arvosta riippumatta; se vain kalibroi alavirran laskelmia. Ehdotusmoottori mittaa anturin todellisen poljinnopeuden aiemmista sykleistä ja asettaa sen automaattisesti.", "label": "Näytteenottoväli" @@ -1598,13 +1628,17 @@ "label": "Tehoprofiilin aikaväli", "doc": "Säilön koko profiilin power_profile-sensorin attribuutille (tasainen kohtakohtainen keskivattimäärän matriisi, jota käyttävät ulkoiset suunnittelijat kuten EMHASS ja tibber_prices). Pienemmät säiliöt pitävät lyhyet tehopiikit terävinä; suuremmat säiliöt tasoittavat muodon. Oletus 15 min. Vain lukuaika; ei vaikuta tunnistukseen." }, + "notify_live_silent": { + "label": "Äänettömät live-päivitykset", + "doc": "Vain iOS. Päivitä reaaliaikaista edistymistä huomaamattomasti: jokainen päivitys saapuu ilman ääntä tai värinää, matalammalla, akkua säästävällä prioriteetilla, jonka iOS voi hetkeksi niputtaa. Syklin ensimmäinen päivitys (se, joka käynnistää Live Activity -näkymän) sekä aloitus- ja lopetusilmoitukset hälyttävät edelleen normaalisti. Poista tämä käytöstä, jos haluat hälytyksen jokaisesta päivityksestä." + }, "notify_live_sticky": { "label": "Pidä reaaliaikailmoitus napauttamisen jälkeen", "doc": "Vain Android. Tee reaaliaikaisesta edistymisilmoituksesta pysyvä (sticky), jotta sen napauttaminen ei sulje käynnissä olevaa viestiketjua. Pois päältä säilyttää oletustoiminnan, jossa napautus sulkee ilmoituksen." }, "notify_live_click_action": { - "label": "Reaaliaikailmoituksen napauttamiskohde", - "doc": "Vain Android. Mihin reaaliaikaisen edistymisilmoituksen napauttaminen avaa (esim. /lovelace/laundry tai koko URL) sovellusaloitussivun sijaan. Jätä tyhjäksi oletusmääränpäätä varten." + "label": "Ilmoituksen napauttamiskohde", + "doc": "Mihin tämän laitteen ilmoituksen napauttaminen vie, sekä Androidissa että iOS:ssä. Tyhjä avaa WashData-paneelin tälle laitteelle. Syötä koontinäytön polku (esim. /lovelace/laundry) tai koko URL ohjataksesi sen muualle, tai \"none\", jos napautuskohdetta ei haluta lainkaan." }, "door_opens_at_end": { "label": "Ovi avautuu automaattisesti lopussa", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Ohjelmaa muovaavat syklit", "doc": "Mitä syklejä käytetään kunkin ohjelman tehokäyrän muodostamiseen ja valmistuneen syklin täsmäyttämiseen siihen. Valinnan poistaminen estää kyseisen tyypin vaikutuksen ohjelmiisi poistamatta mitään - syklit pysyvät Syklit-luettelossa, ja ne voi silti merkitä tai poistaa. Hyödyllinen, jos et luota tuotuun dataan. Tilastot eivät muutu: ne laskevat aina vain ne syklit, jotka tämä kone on todella ajanut. Kaikkien valintojen poistaminen ohitetaan, koska ohjelma ilman taustalla olevia syklejä ei voisi koskaan täsmätä." + }, + "energy_price_dynamic": { + "label": "Aikapainotettu kustannus", + "doc": "Veloittaa kunkin syklin sillä hinnalla, joka oli voimassa jokaisella sen käyntihetkellä, yhden päättymishetken hinnan sijaan. Koskee vain hintaentiteettiä (kiinteä hinta ei voi muuttua). Ei vaadi lisäasetuksia: hintaa seurataan reaaliaikaisesti syklin käydessä, ja Diagnostiikan alla oleva toiminto Käsittele historia laskee vanhempien syklien kustannukset uudelleen recorderin tiedoista. Pois = perinteinen toiminta, yksi lopussa lukittu hinta." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Jaetaan…", "deleting": "Poistetaan…", "imported": "Tuotu", - "preparing": "Valmistellaan…" + "preparing": "Valmistellaan…", + "unlabelling": "Poistetaan merkintää…" }, "suggestion": { "both_agree": "WashData suosittelee", @@ -1775,10 +1814,6 @@ "higher": "Vain erittäin varmat osumat vahvistetaan – useammat syklit jäävät merkitsemättä", "lower": "Useammat syklit täsmäytetään – osa voi olla hieman väärin" }, - "running_dead_zone": { - "higher": "Lyhyet tehohuiput tyhjäkäynnin aikana ohitetaan – vähemmän häiriöherkkä", - "lower": "Reagoi lyhyempiin tehoimpulsseihin – havaitsee nopeat transientit" - }, "start_threshold_w": { "higher": "Välttää väärät käynnistykset lyhyistä tehohuipuista", "lower": "Tunnistaa vähätehoiset ohjelmat – voi aktivoitua häiriöistä" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} puhtaasta jaksosta ({pct}%) sisälsi yli 60s kestäneen sisäisen tauon, joka jatkui; vaatii {val} loppuvahvistusta.", "learning_confidence": "p05-luottamus {cycles} käyttäjän merkitsemälle jaksolle ({p05}); tämän alle pyydetään vahvistusta.", "match_interval": "Perustuu havaittuun päivitystahtiin (mediaani={median}s) * 10.", + "match_interval_capped": "Rajoitettu niin, että {persistence} peräkkäistä täsmäystä mahtuu {pct} prosenttiin lyhimmästä ohjelmasta ({shortest}s); pelkkä päivitystahti (mediaani={median}s) olisi ehdottanut pidempää väliä.", + "match_interval_floored": "Pelkkä lyhin ohjelma ({shortest}s) rajaisi tämän arvoon {cap}s, mikä on alle {minimum}s vähimmäisarvon, joten arvo pidetään vähimmäisarvossa: {persistence} peräkkäistä täsmäystä kestää {budget}s, mikä on yli {pct} prosenttia kyseisestä ohjelmasta. Täsmäytys tehdään vain, kun uusi lukema saapuu (mediaani={median}s), joten vähimmäisarvo ei maksa mitään.", "max_duration_ratio": "Perustuu merkittyjen jaksojen kestoihin (p95={p95}).", "min_active": "Perustuu pienimpään aktiiviseen tehoon ({min}W), joka havaittiin viime jaksossa.", "min_duration_ratio": "Pidetään mahdollisimman matalana, jotta ohjelma tunnistetaan jakson alkuvaiheessa; luottamus- ja epäselvyysrajat estävät ennenaikaiset päätökset.", @@ -1921,6 +1958,7 @@ "recording_stopped": "Tallennus lopetettiin", "review_saved": "Arvostelu tallennettu", "settings_conflicts": "Korjaa korostetut asetusten ristiriidat ennen tallentamista.", + "settings_no_changes": "Ei muutoksia tallennettavaksi", "settings_reverted": "Asetukset palautettu; integroinnin uudelleenlataus", "settings_saved": "Asetukset tallennettu; integroinnin uudelleenlataus", "suggestions_applied": "Ehdotuksia otettu käyttöön; integroinnin uudelleenlataus", @@ -1979,7 +2017,11 @@ "export_selective_done": "Vienti ladattu", "import_selective_done": "Tuotu {profiles} profiilia ja {cycles} jaksoa", "hist_csv_required": "Lataa ensin CSV-tiedosto tai liitä sen sisältö", - "file_read_failed": "Tiedostoa ei voitu lukea" + "file_read_failed": "Tiedostoa ei voitu lukea", + "unlabel_done": "{count} syklin merkintä poistettu", + "unlabel_failed": "Merkinnän poisto epäonnistui: {error}", + "odometer_saved": "Syklien kokonaismäärä tallennettu", + "odometer_save_failed": "Kokonaismäärän tallennus epäonnistui: {error}" }, "trend": { "down": "Trendikäs alaspäin", @@ -1998,10 +2040,11 @@ "date_tip": "Syklin alkamispäivä ja -aika.", "duration_tip": "Syklin kokonaiskesto alusta loppuun.", "energy_tip": "Kulutettu kokonaisenergia (kWh). Laskettu integroimalla teho ajan yli.", - "cost_tip": "Tämän syklin energiakustannus, lukittu valmistuessa käyttäen silloin voimassa ollutta hintaa (energia x hinta per kWh). Aseta hinta Asetuksista täyttääksesi sen.", + "cost_tip": "Tämän syklin energiakustannus, lukittu valmistuessa. Kun Aikapainotettu kustannus on käytössä, tehokäyrä veloitetaan kullakin hetkellä voimassa olleella hinnalla; muuten energia x yksi lopussa voimassa ollut hinta. Aseta hinta Asetuksista täyttääksesi sen.", "confidence_tip": "Profiilin osumaluottamus (0-100%). Kuinka tarkasti syklin tehokäyrä osui tunnistettuun ohjelmaan.", "health_tip": "ML-syklin kunto (korkeampi = parempi). Napsauta sykliä tarkastellaksesi ja arvioidaksesi sitä.", - "flags_tip": "Syklin tarkistus-, poikkeama- ja lähdeliput. Siirrä hiiri kuvakkeen päälle saadaksesi lisätiedot." + "flags_tip": "Syklin tarkistus-, poikkeama- ja lähdeliput. Siirrä hiiri kuvakkeen päälle saadaksesi lisätiedot.", + "cost_dynamic_tip": "Aikapainotettu: veloitettu syklin aikana voimassa olleella hinnalla (toteutunut {price} {cur}/kWh)." }, "stat": { "avg": "ka.", @@ -2028,9 +2071,9 @@ "finished": "Jakso saavutti lopputilan ja päättyi." }, "store": { + "search_ph2": "Hae merkin tai mallin mukaan…", "your_model_tip": "Tämä on laite, jonka määritit asetuksissa", "your_model": "Omasi", - "search_brand_ph": "Hae merkin mukaan…", "programs_count": "Ohjelmat: {n}", "browse": "Selaa", "device": "Laite", @@ -2075,7 +2118,8 @@ "golden": "Uudelleenkäsittely: vertailujaksojen täydennys", "suggestions": "Uudelleenkäsittely: ehdotukset", "ml_training": "Uudelleenkäsittely: ML-koulutus", - "health": "Uudelleenkäsittely: syklien kunto" + "health": "Uudelleenkäsittely: syklien kunto", + "costs": "Uudelleenkäsittely: energiakustannukset" }, "pg_sweep": { "optimize": "Optimointi: {param}" diff --git a/custom_components/ha_washdata/translations/panel/fr.json b/custom_components/ha_washdata/translations/panel/fr.json index 0feb0f69..67c5880e 100644 --- a/custom_components/ha_washdata/translations/panel/fr.json +++ b/custom_components/ha_washdata/translations/panel/fr.json @@ -1,5 +1,7 @@ { "badge": { + "quiet_tail_tip": "Vers la fin, ce programme reste silencieux pendant environ {mins} min, puis consomme environ {watts} W pendant {secs} s avant de se terminer. Observé sur {seen} des {measured} cycles mesurés : l'appareil ne le fait pas à chaque utilisation.", + "quiet_tail": "silence final ~{mins} min", "artifact_tip": "{n} anomalies détectées (par exemple, porte ouverte en cours de cycle) : ouvrez-les pour les voir sur le graphique", "auto": "(détecté automatiquement)", "built_in_tag": "intégré", @@ -15,7 +17,7 @@ "needs_review": "Besoin d'un examen", "overrun": "A couru plus longtemps que d'habitude", "poor_fit": "qualité médiocre", - "poor_fit_tip": "Historique des matchs incohérent : envisagez de reconstruire ce profil", + "poor_fit_tip": "Historique de correspondance incohérent : envisagez de reconstruire ce profil", "restart_gap_tip": "{n} coupure redémarrage HA : la courbe de puissance a un trou", "reviewed": "Révisé", "steady": "→ stable", @@ -29,11 +31,14 @@ "approved": "Approuvé", "awaiting": "En attente d'approbation", "imported_tip": "Importé depuis la boutique communautaire. Utilisé uniquement pour la correspondance, non compté dans les statistiques.", + "unmatchable": "correspondance impossible", "not_importable": "n/a ici", "exists": "existe déjà", - "backfilled_tip": "Détecté dans un historique de puissance importé. Influence uniquement la correspondance des programmes, non compté dans les statistiques." + "backfilled_tip": "Détecté dans un historique de puissance importé. Influence uniquement la correspondance des programmes, non compté dans les statistiques.", + "armed": "(s'applique au prochain cycle)" }, "btn": { + "reset_zoom": "Réinitialiser le zoom", "set_brand_model": "Définir la marque et le modèle", "refresh_catalog": "Rafraîchir le catalogue", "add_device": "+ Ajouter un appareil", @@ -167,6 +172,8 @@ "trim": "Rogner", "trim_split": "Rogner / Diviser", "undo": "Annuler", + "unlabel_selected": "Retirer l'étiquette de la sélection ({n})", + "unlabel_selected_tip": "Retire l'étiquette de profil des cycles sélectionnés. Ils restent dans votre historique et pourront être réétiquetés plus tard ; ils cessent simplement d'influencer ce profil.", "use": "Utiliser", "wipe_all": "Effacer toutes les données", "delete_selected": "Supprimer la sélection ({n})", @@ -174,7 +181,6 @@ "pg_apply_best": "Appliquer le meilleur", "pg_apply_to_settings": "Enregistrer dans les réglages", "pg_apply_to_settings_tip": "Copier les valeurs que vous avez modifiées ici dans les réglages actifs de cet appareil", - "search": "Rechercher", "import": "Importer", "disconnect": "Déconnecter", "connect_github": "Connecter GitHub", @@ -201,7 +207,8 @@ "hist_read_recorder": "Lire depuis Home Assistant", "hist_scan": "Rechercher des cycles", "hist_import_n": "Importer {n} cycles", - "hist_goto_cycles": "Voir les cycles" + "hist_goto_cycles": "Voir les cycles", + "save_total_cycles": "Enregistrer le total" }, "conflict": { "anti_wrinkle_exit": { @@ -214,6 +221,12 @@ }, "attn_sub": "Corriger les conflits avant d'enregistrer", "attn_title": "Conflits de paramètres : {n}", + "price_entity": { + "power_sensor": "Il s'agit du Capteur de puissance de l'appareil, pas d'un prix. Une entité de prix prévaut sur le Prix de l'énergie statique ci-dessous, donc les cycles seraient facturés à des watts par kWh - videz ce champ.", + "energy_sensor": "Il s'agit de l'Entité du compteur d'énergie de l'appareil, pas d'un prix. Une entité de prix prévaut sur le Prix de l'énergie statique ci-dessous, donc les cycles seraient facturés à la valeur du compteur par kWh - videz ce champ.", + "device_class": "Ce capteur mesure {dc}, pas un prix par kWh. Une entité de prix prévaut sur le Prix de l'énergie statique ci-dessous - videz ce champ ou pointez-le vers un capteur de tarif.", + "unit": "Ce capteur est exprimé en {unit}, ce qui n'est pas un prix par kWh. Une entité de prix prévaut sur le Prix de l'énergie statique ci-dessous - videz ce champ ou pointez-le vers un capteur de tarif." + }, "settings_banner": "Conflits de paramètres : {n}. Vérifiez les sections surlignées et corrigez-les avant d'enregistrer.", "settings_banner_btn": "Aller au premier", "confidence": { @@ -301,7 +314,7 @@ "pg_settings_source": "Origine des réglages", "phase_catalog": "Catalogue de phases", "playground": "Bac à sable", - "process_history": "Historique du processus", + "process_history": "Traiter l'historique", "settings_history": "Historique des paramètres", "status": "Statut", "status_graph": "Graphique d'état", @@ -312,12 +325,12 @@ "wipe_history": "Effacer l'historique", "recommendations": "Recommandations ({n})", "pg_detection_params": "Paramètres de détection", - "pg_timeline": "Chronologie des événements", "pg_outcome": "Résultat de la simulation", "pg_across_cycles": "Sur l'ensemble de vos cycles", "community_store": "Boutique communautaire", "online_account": "Boutique communautaire et fonctionnalités en ligne", - "import_power_history": "Importer l'historique de puissance" + "import_power_history": "Importer l'historique de puissance", + "service_status": "État de l'entretien" }, "health": { "fair": "Qualité de profil acceptable", @@ -325,6 +338,10 @@ "poor": "⚠ Qualité de profil médiocre" }, "lbl": { + "envelope_position_tip": "Position sur la courbe enregistrée du programme correspondant, {pct} %. Elle est mesurée en alignant ce cycle sur le profil plutôt qu'en comptant le temps, elle reste donc juste lorsqu'un cycle est plus long ou plus court que d'habitude. Elle est actualisée lorsque l'appareil est silencieux et peut donc être en retard pendant une phase active.", + "envelope_position": "courbe {pct} %", + "aria_playground_chart3": "Graphique interactif de puissance du cycle : survolez pour lire le temps/la puissance, faites défiler ou pincez pour zoomer, faites glisser pour vous déplacer", + "device_unresolved": "Appareil indisponible ({id})", "drag_to_resize": "Faites glisser pour redimensionner", "actions": "Actions", "activity": "Activité", @@ -460,7 +477,7 @@ "profile_index": "index du profil →", "profile_name": "Nom du profil", "program": "Programme", - "program_tip": "Remplacez le profil associé au cycle en cours. La détection automatique permet à l'intégration de choisir automatiquement la meilleure correspondance. Épinglez un programme spécifique pour forcer la correspondance quand la détection automatique est incorrecte ou que vous savez ce qui tourne.", + "program_tip": "Remplacez le profil associé au cycle en cours. La détection automatique permet à l'intégration de choisir automatiquement la meilleure correspondance. Épinglez un programme spécifique pour forcer la correspondance quand la détection automatique est incorrecte ou que vous savez ce qui tourne. Sélectionnez-en un avant de démarrer l'appareil et il sera appliqué dès le début du prochain cycle.", "progress": "Progrès", "raw_socket": "Signal brut", "read_only": "Lecture seule", @@ -486,7 +503,7 @@ "settings_basic": "De base", "settings_detail_level": "Niveau de détail des paramètres", "settings_overrides": "Remplacements de paramètres", - "show_debug": "Afficher la carte de débogage du match en direct sur la page Statut (confiance, ambiguïté, meilleurs candidats)", + "show_debug": "Afficher la carte de débogage de la correspondance en direct sur la page Statut (confiance, ambiguïté, meilleurs candidats)", "show_expected": "Afficher la superposition de courbe attendue (profil correspondant, orange)", "show_raw": "Afficher le commutateur de signal brut de la prise dans le graphique de puissance en direct", "sparkline": "Tendance récente de la durée des cycles", @@ -495,6 +512,7 @@ "stage4": "Étape 4 – concordance", "status": "Statut", "tail_trim": "Rognage de fin (s)", + "tap_to_dismiss": "appuyez sur le graphique pour fermer", "timer_auto_pause": "Pause automatique", "timer_min": "min", "timer_msg_placeholder": "Message (facultatif, {device}/{program}/{minutes})", @@ -531,9 +549,7 @@ "pg_time_left_model": "Temps restant (modèle)", "pg_time_left_model_tip": "Temps restant estimé par le modèle (estimateur de phase + mélange ML), pas un compte à rebours fixe", "phase": "Phase", - "pg_mode_simulate": "Simuler", "pg_mode_history": "Tester sur l'historique", - "pg_mode_sweep": "Balayage", "pg_ev_detected": "Détecté", "pg_ev_match": "Correspondance confirmée", "pg_ev_match_changed": "Correspondance modifiée", @@ -572,14 +588,11 @@ "pg_stress_idle_w_desc": "Remplace le plancher de veille détecté automatiquement (laisser vide pour auto).", "pg_stress_toggle": "Tester la terminaison au repos", "pg_stress_toggle_desc": "Simule l'appareil maintenu à sa consommation en veille après la fin de l'enregistrement – indique si et quand WashData arrête le cycle.", - "pg_sweep_2d": "Carte thermique 2D", - "pg_sweep_param_y": "2e paramètre", "pg_obj_match": "Précision de correspondance", "pg_obj_endtiming": "Précision du moment de fin", "pg_obj_falseend": "Taux de fausses fins", "pg_obj_overrun": "Écart de durée", "pg_obj_ambiguity": "Taux d'ambiguïté", - "pg_best_combo": "meilleure combinaison", "pg_mode_optimize": "Optimiser", "pg_group_matching": "Correspondance de programme", "pg_zoom_hint": "double-cliquez pour réinitialiser le zoom", @@ -658,7 +671,12 @@ "task_history_import_apply": "Importation des cycles", "evidence_real_cycles": "Cycles effectués par cette machine", "evidence_reference_cycles": "Téléchargés depuis la boutique communautaire", - "evidence_backfill_cycles": "Trouvés dans un historique de puissance importé" + "evidence_backfill_cycles": "Trouvés dans un historique de puissance importé", + "zoom_hint_touch": "pincer pour zoomer", + "zoom_hint_touch_zoomed": "pincer pour zoomer · appuyer deux fois pour réinitialiser", + "cycles_since_service": "{since} / {total} cycles", + "total_cycles_run": "cycles effectués au total", + "correct_total_cycles": "Corriger le total" }, "log": { "all_levels": "Tous niveaux", @@ -708,7 +726,7 @@ "merge_cycles": "Fusionner {n} cycles", "new_group": "Nouveau groupe de profils", "new_phase": "Nouvelle phase", - "process_history_msg": "Relancez la correspondance, actualisez les suggestions, réentraînez les modèles ML (si activés) et recalculez la santé des cycles sur tous les cycles enregistrés. Cela peut prendre un moment.", + "process_history_msg": "Relancez la correspondance, actualisez les suggestions, réentraînez les modèles ML (si activés), recalculez le coût des cycles d'après les prix de l'énergie enregistrés et recalculez la santé des cycles sur tous les cycles enregistrés. Cela peut prendre un moment.", "process_history_ok": "Traiter", "process_history_title": "Traiter l'historique", "process_recording": "Enregistrement du processus", @@ -741,6 +759,7 @@ "history_import": "Importer l'historique de puissance" }, "msg": { + "pg_canvas_empty3": "Choisissez un cycle ci-dessus et appuyez sur Exécuter pour le simuler. Survolez ensuite pour lire les valeurs, faites défiler ou pincez pour zoomer et faites glisser pour vous déplacer.", "tail_trim_hint": "Nombre de secondes à supprimer à la fin", "store_sibling_hint": "Rien de partagé pour votre modèle exact ? Un modèle proche de la même marque constitue généralement un bon point de départ.", "store_declare_appliance": "Indiquez à WashData l'appareil que vous possédez et cet onglet affichera les configurations que d'autres personnes ont partagées pour celui-ci. Vous pouvez aussi saisir une marque ci-dessus pour explorer.", @@ -754,7 +773,7 @@ "artifact_spike_detail": "A dépassé la bande de puissance habituelle pendant environ {n} s.", "auto_label_intro": "Attribuez des profils à des cycles non étiquetés dont la confiance de correspondance efface le seuil.", "automations_intro": "WashData déclenche les événements {start} / {end} et expose des entités, les notifications et actions sont donc mieux construites comme des automatisations Home Assistant normales. Les automatisations utilisant cet appareil apparaissent ci-dessous.", - "cleanup_intro": "Chaque cycle étiqueté superposé. Cochez les valeurs aberrantes et supprimez-les pour nettoyer le profil.", + "cleanup_intro": "Chaque cycle étiqueté superposé. Cochez les valeurs aberrantes, puis retirez-leur l'étiquette (conservés dans l'historique, mais n'influençant plus ce profil) ou supprimez-les définitivement.", "clear_debug_hint": "Supprimez les données de débogage stockées pour libérer de l'espace.", "collecting_data": "Collecte de données. Cycles encore nécessaires avant que l'affinage puisse commencer : {need} ({current}/{min}).", "compare_overlay_profiles": "Profils de superposition (faible)", @@ -853,7 +872,7 @@ "preferences_admin": "Préférences, panneau et contrôle d'accès", "preferences_adv": "Préférences", "prefs_personal": "Celles-ci s'appliquent uniquement à votre compte Home Assistant.", - "process_history_hint": "Relancez la correspondance sur tous les cycles enregistrés, actualisez les suggestions de réglage, réentraînez les modèles ML (si activés) et recalculez la santé des cycles. À exécuter après un lot de révisions.", + "process_history_hint": "Relancez la correspondance sur tous les cycles enregistrés, actualisez les suggestions de réglage, réentraînez les modèles ML (si activés), recalculez le coût des cycles d'après les prix de l'énergie enregistrés et recalculez la santé des cycles. À exécuter après un lot de révisions.", "profile_deleted": "Profil supprimé", "profile_poor_health_detail": "Les cycles attribués à ce profil ont des formes incohérentes ou un faible niveau de confiance. Pensez à reconstruire l’enveloppe ou à revoir les cycles étiquetés.", "profiles_intro": "Cliquez sur un profil pour les statistiques, les phases et le nettoyage. Regroupez des programmes presque identiques (même forme/durée, température ou essorage différents) afin de faire correspondre de manière fiable les choix entre eux.", @@ -936,9 +955,10 @@ "advisory_poor_health": "'{name}' a un faible score de correspondance - ses cycles récents varient beaucoup ou correspondent mal. Vérifiez ses cycles ou réenregistrez le profil pour que la reconnaissance et les estimations de temps restent précises.", "advisory_shape_drift": "'{name}' s'est nettement éloigné de sa courbe de puissance d'origine. L'appareil a peut-être changé de comportement avec le temps (p. ex. calcaire, usure). Envisagez de réenregistrer ce profil avec des cycles récents.", "advisory_shape_drift_corr": "'{name}' s'est nettement éloigné de sa courbe de puissance d'origine (corrélation {corr}). L'appareil a peut-être changé de comportement avec le temps (p. ex. calcaire, usure). Envisagez de réenregistrer ce profil avec des cycles récents.", + "advisory_unmatchable": "'{name}' ne pourra jamais correspondre à un cycle : aucun cycle avec des données de puissance ne lui est associé, donc WashData n'a rien à quoi comparer un cycle en cours. Étiquetez un cycle de ce programme (ou enregistrez-en un) pour le réintégrer dans la correspondance.", + "advisory_unmatchable_no_duration": "'{name}' ne pourra jamais correspondre à un cycle : il possède un cycle avec des données de puissance mais sans durée exploitable, donc WashData n'a aucune longueur sur laquelle caler une correspondance. Réenregistrez ce programme, ou définissez sa durée attendue, pour le réintégrer dans la correspondance.", "pg_no_alerts": "Aucun problème détecté lors de cette exécution.", "pg_override_active": "Utilisation de vos paramètres modifiés vs actuels", - "pg_history_intro": "Rejouez vos cycles récents avec le détecteur et le moteur de correspondance réels. Modifiez d'abord un paramètre de détection dans Simuler pour voir ici une comparaison avant/après.", "pg_history_empty": "Appuyez sur Exécuter pour rejouer vos cycles.", "pg_history_summary": "{detected}/{total} détectés · {correct} correctement reconnus", "pg_sweep_no_metric": "Données insuffisantes pour évaluer cet objectif.", @@ -1015,11 +1035,16 @@ "hist_scan_failed": "L'analyse a échoué.", "hist_scan_expired": "Cette analyse n'est plus disponible. Veuillez relancer l'analyse.", "hist_import_failed": "L'importation a échoué.", - "imported_history_readonly": "Détecté dans un historique de puissance importé. Il influence la correspondance des programmes mais n'est pas compté dans vos statistiques, et ne peut être ni rogné ni divisé. Étiquetez-le pour nommer le programme." + "imported_history_readonly": "Détecté dans un historique de puissance importé. Il influence la correspondance des programmes mais n'est pas compté dans vos statistiques, et ne peut être ni rogné ni divisé. Étiquetez-le pour nommer le programme.", + "odometer_intro": "Ce total ne fait qu'augmenter. Supprimer l'enregistrement d'un cycle ne le modifie pas, et il continue de compter au-delà de la limite d'historique conservé, afin que les rappels d'entretien restent exacts.", + "odometer_correct_hint": "Renseignez cette valeur si WashData a enregistré un cycle qui n'a jamais eu lieu, ou si l'appareil avait déjà effectué des cycles avant l'installation de WashData.", + "no_reminders_set": "Aucun rappel d'entretien n'est encore configuré.", + "toast_program_armed": "Programme prévu pour le prochain cycle : {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Lave-vaisselle : secondes de silence après la durée prévue avant de libérer l'attente de vidange de fin de cycle", "smart_termination_duration_ratio": "Fraction de la durée prévue du programme correspondant qu'un cycle doit atteindre avant que la Terminaison intelligente puisse le terminer plus tôt ; abaissez-la pour les machines dépendant de la charge ou de la température", + "anti_crease_finalize_ratio": "Fraction de la durée prévue du programme correspondant qu'un cycle doit atteindre avant que la queue de culbutage anti-froissement puisse être finalisée ; abaissez-la sur les sèche-linge à longue queue de culbutage, laissez les lave-linge à la valeur par défaut", "anti_wrinkle_enabled": "Absorber les impulsions de culbutage après la phase principale au lieu de les lire comme de nouveaux cycles", "anti_wrinkle_exit_power": "La puissance doit tomber sous ce seuil entre les impulsions pour que l'anti-froissement reste actif", "anti_wrinkle_idle_timeout": "Temps de silence autorisé entre deux impulsions de culbutage avant la fin de l'anti-froissement", @@ -1045,7 +1070,8 @@ "dtw_ddtw_scale": "Étape 3 : distance de demi-saturation DDTW ; plus petit = plus sensible à la forme (défaut 30)", "dtw_refine_top_n": "Étape 3 : candidats réévalués par DTW ; augmentez à 7-9 si le profil correct se classe 4e ou 5e (défaut 5)", "duration_scale": "Étape 4 : log-ratio où la concordance de durée est divisée par deux ; plus petit = pénalité plus stricte (défaut 0,175)", - "energy_scale": "Étape 4 : log-ratio où la concordance d'énergie est divisée par deux ; plus petit = pénalité plus stricte (défaut 0,25)" + "energy_scale": "Étape 4 : log-ratio où la concordance d'énergie est divisée par deux ; plus petit = pénalité plus stricte (défaut 0,25)", + "curve_preroll_seconds": "Secondes de relevés issus de tentatives de démarrage avortées reportées au début de la courbe d'un cycle ; 0 = désactivé" }, "phase_desc": { "anti_crease": "Courts culbutages occasionnels après completion pour réduire le froissement du linge.", @@ -1143,6 +1169,10 @@ "label": "Libération au silence du séchage passif", "doc": "Une fois que le cycle dépasse sa durée prévue, combien de temps le lave-vaisselle doit rester silencieux (sous le Seuil d'arrêt) avant que WashData cesse d'attendre une vidange finale et termine le cycle. Augmentez cette valeur si votre machine a une longue phase de séchage silencieuse avant une vidange finale tardive qui n'est pas détectée - une fenêtre plus large permet à la durée apprise de suivre la dérive saisonnière (eau d'arrivée plus froide = cycles plus longs) au lieu de se figer sur l'ancienne moyenne. Elle ne fait que raccourcir l'attente par rapport au plafond interne de 30 minutes pour le pic de fin, jamais la prolonger." }, + "anti_crease_finalize_ratio": { + "label": "Ratio de finalisation anti-froissement", + "doc": "À quel point de la durée prévue du programme correspondant un cycle doit être parvenu avant que WashData puisse décider que le culbutage de faible puissance qu'il observe est la queue anti-froissement et terminer le cycle à cet endroit. La durée prévue est la moyenne du programme : sur un sèche-linge dont la durée de séchage par sonde suit la charge, la plupart des cycles n'atteignent jamais 98 % de cette moyenne et la queue reste en attente jusqu'au délai d'expiration de secours, soit souvent 15 à 40 minutes après l'arrêt réel de la machine. Abaissez cette valeur (par exemple 0,75) sur un sèche-linge de ce type. Laissez les lave-linge à la valeur par défaut : un lave-linge passe la majeure partie de son cycle sous la Puissance maximale anti-froissement, cette fraction est donc la seule chose qui sépare une phase calme en milieu de lavage de la queue, et l'abaisser peut couper un lavage en deux. Laissez vide pour la valeur par défaut (0,98). Ce réglage est distinct du Ratio de terminaison intelligente, qui conditionne une vérification de fin anticipée différente." + }, "anti_wrinkle_enabled": { "doc": "Reconnaissez les courtes impulsions de culbutage de faible puissance qu'un sèche-linge émet après la phase de chauffage principale et conservez-les attachées au cycle terminé au lieu de les lire comme de nouveaux cycles.", "label": "Activer la détection anti-froissement" @@ -1175,6 +1205,10 @@ "doc": "Les cycles plus courts que cela sont rejetés comme cycles fantômes (essais, ouverture de la porte pour ajouter une chaussette).", "label": "Durée minimale du cycle" }, + "curve_preroll_seconds": { + "label": "Amorce de courbe", + "doc": "Combien de secondes de relevés issus de tentatives de démarrage avortées WashData peut reporter au début de la courbe d'un cycle. La courbe d'un cycle commence normalement à la tentative de démarrage qui finit par aboutir : sur une machine qui tâtonne plusieurs fois avant de se lancer (sélection du programme, verrouillage de la porte, premier remplissage), les premières minutes d'activité réelle manquent dans chaque courbe. Réglez cette valeur un peu au-dessus de la plus longue mise en route de ce type sur votre machine (300 s suffisent largement dans la plupart des cas). 0 désactive la fonction. Attention : l'activer avance le début enregistré, de sorte que les cycles enregistrés avant et après la modification portent des durées différentes pour un même programme ; les moyennes apprises vont dériver jusqu'à ce que les anciens cycles disparaissent de l'historique." + }, "delay_confirm_seconds": { "doc": "L'alimentation doit rester dans la bande de veille pendant ce temps avant que l'appareil ne soit traité comme en attente de démarrage plutôt que comme étant en fonctionnement.", "label": "Secondes de confirmation" @@ -1212,7 +1246,7 @@ "label": "Nombre de répétitions de fin" }, "energy_price_entity": { - "doc": "Capteur avec le prix actuel de l'électricité par kWh (par exemple un tarif dynamique). Prévaut sur le prix statique ci-dessous. Chaque cycle gèle le prix en vigueur à sa fin.", + "doc": "Capteur avec le prix actuel de l'électricité par kWh (par exemple un tarif dynamique). Doit être un prix, pas le capteur de puissance ni le compteur d'énergie de la prise elle-même : il prévaut sur le prix statique ci-dessous, donc un compteur kWh placé ici facture chaque cycle à la valeur du compteur au lieu de votre tarif. Avec Coût pondéré dans le temps activé, chaque cycle est facturé au prix en vigueur à chaque instant où il a tourné ; sinon, le prix en vigueur à sa fin lui est figé.", "label": "Entité de prix de l'énergie" }, "energy_price_static": { @@ -1240,7 +1274,7 @@ "label": "Inverser le déclencheur externe (déclencher sur ARRÊT)" }, "learning_confidence": { - "doc": "Si le score de correspondance se situe entre ceci et Auto-Label Confidence, une notification de retour vous demande de vérifier le programme identifié. En dessous de ce score, le match est trop incertain pour faire surface. Doit être conservé en dessous du niveau de confiance Auto-Label.", + "doc": "Si le score de correspondance se situe entre celui-ci et la Confiance d'étiquetage automatique, une notification de retour vous demande de vérifier le programme identifié. En dessous de ce score, la correspondance est trop incertaine pour être affichée. Doit rester en dessous de la Confiance d'étiquetage automatique.", "label": "Confiance d'apprentissage" }, "linked_device": { @@ -1308,7 +1342,7 @@ "label": "Déclencher des événements HA pour les notifications" }, "notify_icon": { - "doc": "Icône mdi facultative pour la notification (par exemple mdi:washing-machine). Laissez vide pour la valeur par défaut de la plateforme.", + "doc": "La petite icône affichée sur la notification (par exemple mdi:washing-machine). Android la dessine dans la barre d'état ; iOS l'affiche à la place de l'icône de l'application et nécessite l'application compagnon 2026.8 ou plus récente. Laissez vide pour la valeur par défaut de la plateforme.", "label": "Icône de notification" }, "notify_live_chronometer": { @@ -1408,7 +1442,7 @@ "label": "Seuil de correspondance" }, "profile_unmatch_threshold": { - "doc": "Si un match en direct à mi-cycle tombe en dessous de ce score, l'identification provisoire est effacée. Gardez-le un peu en dessous du seuil de correspondance afin qu'une brève baisse de similarité ne ramène pas l'affichage à un niveau inégalé.", + "doc": "Si une correspondance en direct en milieu de cycle tombe en dessous de ce score, l'identification provisoire est effacée. Gardez-le un peu en dessous du Seuil de correspondance afin qu'une brève baisse de similarité ne fasse pas repasser l'affichage à l'état sans correspondance.", "label": "Seuil de non-correspondance" }, "progress_reset_delay": { @@ -1419,10 +1453,6 @@ "doc": "Secondes pendant lesquelles une pompe peut fonctionner en continu avant d'être signalée comme étant éventuellement bloquée (déclenche l'événement de pompe bloquée).", "label": "Durée de blocage de la pompe" }, - "running_dead_zone": { - "doc": "Après le démarrage d'un cycle, les baisses de puissance dans cette fenêtre sont ignorées. Les machines à laver se remplissent d'eau froide (tombant près de 0 W avant de chauffer) - sans cette protection, cette phase de remplissage ressemble à une fin de cycle. Cela ne saute PAS de données : la trace de pleine puissance est enregistrée à partir de T=0. Le moteur de suggestion mesure le modèle de démarrage réel de votre machine et le dimensionne automatiquement.", - "label": "Zone morte en cours d'exécution" - }, "sampling_interval": { "doc": "Temps prévu entre les lectures du capteur - utilisé pour dimensionner la fenêtre de lissage et démarrer correctement l'anti-rebond. Chaque mise à jour du capteur est capturée quelle que soit cette valeur ; il calibre uniquement les calculs en aval. Le moteur de suggestion mesure la cadence réelle de votre capteur à partir des cycles passés et la définit automatiquement.", "label": "Intervalle d'échantillonnage" @@ -1448,7 +1478,7 @@ "label": "Énergie de démarrage" }, "start_threshold_w": { - "doc": "La puissance doit augmenter au-dessus de ce niveau pour confirmer qu’un cycle a commencé. Un réglage trop bas provoque des faux démarrages à partir de l'alimentation de veille ; des programmes trop élevés et qui démarrent lentement (remplissage à froid) sont manqués. Le moteur de suggestion règle cette valeur juste au-dessus de la puissance active la plus basse observée par la machine.", + "doc": "La puissance doit dépasser ce niveau pour confirmer qu'un cycle a commencé. Un réglage trop bas provoque de faux démarrages dus à la puissance de veille ; un réglage trop élevé fait manquer les programmes à démarrage lent (remplissage à froid). Le moteur de suggestion règle cette valeur juste au-dessus de la puissance active la plus basse observée sur la machine.", "label": "Seuil de démarrage" }, "stop_threshold_w": { @@ -1512,7 +1542,6 @@ "doc": "Facultatif. Le modèle de l'appareil, choisi dans le catalogue de la communauté une fois la marque définie. Si votre modèle n'y figure pas, vous pouvez l'ajouter au catalogue." }, "show_contributor": { - "label": "Afficher les noms des contributeurs", "doc": "Afficher l'attribution « par » sur les appareils de la communauté et les cycles de référence." }, "enable_phase_matching": { @@ -1551,13 +1580,17 @@ "label": "Intervalle du profil de puissance", "doc": "Taille des créneaux pour l'attribut de capteur power_profile par profil (le tableau plat de watts moyens par créneau consommé par des planificateurs externes tels que EMHASS et tibber_prices). Des créneaux plus petits conservent les pics de puissance courts et nets ; des créneaux plus grands lissent la forme. Par défaut 15 min. Lecture seule ; n'affecte pas la détection." }, + "notify_live_silent": { + "label": "Mises à jour en direct silencieuses", + "doc": "iOS uniquement. Actualise la progression en direct en toute discrétion : chaque mise à jour arrive sans son ni vibration, avec une priorité réduite qui économise la batterie et qu'iOS peut regrouper pendant un court instant. La première mise à jour d'un cycle (celle qui lance l'activité en direct) ainsi que les notifications de début et de fin continuent d'alerter normalement. Désactivez cette option pour être alerté à chaque mise à jour." + }, "notify_live_sticky": { "label": "Conserver la notification en direct au toucher", "doc": "Android uniquement. Rend la notification de progression en direct persistante (épinglée) afin que la toucher ne la ferme pas. Désactivé conserve le comportement par défaut où un toucher la ferme." }, "notify_live_click_action": { - "label": "Destination au toucher de la notification en direct", - "doc": "Android uniquement. Où s'ouvre un toucher sur la notification de progression en direct (par exemple /lovelace/laundry, ou une URL complète) au lieu de la page d'accueil de l'application. Laissez vide pour la valeur par défaut." + "label": "Destination au toucher de la notification", + "doc": "Où s'ouvre un toucher sur une notification de cet appareil, sur Android comme sur iOS. Vide ouvre le panneau WashData sur cet appareil. Saisissez un chemin de tableau de bord (par exemple /lovelace/laundry) ou une URL complète pour l'ouvrir ailleurs, ou « none » pour aucune destination au toucher." }, "door_opens_at_end": { "label": "La porte s'ouvre automatiquement en fin de cycle", @@ -1570,6 +1603,10 @@ "profile_evidence_sources": { "label": "Cycles qui façonnent un programme", "doc": "Quels cycles servent à construire la courbe de puissance de chaque programme et à y comparer un cycle terminé. Décocher un type l'empêche de façonner vos programmes sans rien supprimer - les cycles restent dans votre liste Cycles et peuvent toujours être étiquetés ou supprimés. Utile si vous ne faites pas confiance aux données importées. Les statistiques ne sont pas concernées : elles ne comptent toujours que les cycles que cette machine a réellement effectués. Décocher tout est ignoré, car un programme sans aucun cycle derrière lui ne pourrait jamais correspondre." + }, + "energy_price_dynamic": { + "label": "Coût pondéré dans le temps", + "doc": "Facture chaque cycle au prix en vigueur à chaque instant où il a tourné, au lieu du seul prix du moment où il s'est terminé. Ne s'applique qu'à une entité de prix (un prix statique ne peut pas varier). Aucune configuration supplémentaire n'est nécessaire : le prix est suivi en direct pendant le cycle, et Traiter l'historique, dans Diagnostic, recalcule le coût des anciens cycles à partir du recorder. Désactivé = le comportement classique, un seul prix figé à la fin." } }, "setting_group": { @@ -1681,7 +1718,8 @@ "splitting": "Fractionnement…", "deleting": "Suppression…", "imported": "Importé", - "preparing": "Préparation…" + "preparing": "Préparation…", + "unlabelling": "Retrait de l'étiquette…" }, "tab": { "advanced": "Avancé", @@ -1765,6 +1803,7 @@ "recording_stopped": "L'enregistrement s'est arrêté", "review_saved": "Avis enregistré", "settings_conflicts": "Corrigez les conflits de réglages mis en évidence avant d'enregistrer.", + "settings_no_changes": "Aucune modification à enregistrer", "settings_reverted": "Paramètres rétablis ; rechargement de l'intégration", "settings_saved": "Paramètres enregistrés ; rechargement de l'intégration", "suggestions_applied": "Suggestions appliquées ; rechargement de l'intégration", @@ -1819,7 +1858,11 @@ "export_selective_done": "Export téléchargé", "import_selective_done": "{profiles} profil(s) et {cycles} cycle(s) importés", "hist_csv_required": "Chargez d'abord un fichier CSV ou collez son contenu", - "file_read_failed": "Impossible de lire ce fichier" + "file_read_failed": "Impossible de lire ce fichier", + "unlabel_done": "Étiquette retirée de {count} cycle(s)", + "unlabel_failed": "Échec du retrait de l'étiquette : {error}", + "odometer_saved": "Total des cycles enregistré", + "odometer_save_failed": "Impossible d'enregistrer le total : {error}" }, "suggestion": { "both_agree": "WashData recommande", @@ -1866,10 +1909,6 @@ "higher": "Ne valide que les correspondances très confiantes ; plus de cycles restent non étiquetés", "lower": "Fait correspondre plus de cycles ; certains peuvent être légèrement erronés" }, - "running_dead_zone": { - "higher": "Ignore les brèves pointes de puissance au repos – moins sensible au bruit", - "lower": "Réagit à des impulsions plus courtes – capte les activités transitoires rapides" - }, "start_threshold_w": { "higher": "Évite les faux démarrages dus à de brèves pointes de puissance", "lower": "Capte les programmes à faible puissance ; peut se déclencher sur du bruit" @@ -1894,6 +1933,8 @@ "end_repeat_count": "{false}/{total} cycles propres ({pct}%) ont eu une pause interne >60s qui a repris ; exiger {val} confirmation(s) de fin.", "learning_confidence": "Confiance p05 de {cycles} cycles étiquetés par l'utilisateur ({p05}) ; en dessous, une vérification est demandée.", "match_interval": "D'après la cadence de mise à jour observée (médiane={median}s) * 10.", + "match_interval_capped": "Plafonné pour que {persistence} correspondances consécutives tiennent dans {pct} % du programme le plus court ({shortest}s) ; la cadence de mise à jour seule (médiane={median}s) aurait suggéré un intervalle plus long.", + "match_interval_floored": "À lui seul, le programme le plus court ({shortest}s) plafonnerait cette valeur à {cap}s, en dessous du minimum de {minimum}s : elle y est donc maintenue. {persistence} correspondances consécutives prennent {budget}s, soit plus de {pct} % de ce programme. La correspondance ne s'exécute que lorsqu'une mesure arrive (médiane={median}s), le minimum ne coûte donc rien.", "max_duration_ratio": "D'après les durées des cycles étiquetés (p95={p95}).", "min_active": "D'après la puissance active minimale ({min}W) observée lors du dernier cycle.", "min_duration_ratio": "Maintenu aussi bas que possible pour qu'un programme soit reconnu tôt dans le cycle ; les seuils de confiance et d'ambiguïté empêchent les décisions prématurées.", @@ -1951,10 +1992,11 @@ "date_tip": "Date et heure de début du cycle.", "duration_tip": "Durée totale d'exécution du cycle, du début à la fin.", "energy_tip": "Énergie totale consommée (kWh). Calculée en intégrant la puissance dans le temps.", - "cost_tip": "Coût énergétique de ce cycle, figé à la fin en utilisant le prix alors en vigueur (énergie x prix par kWh). Définissez un prix dans les Paramètres pour le renseigner.", + "cost_tip": "Coût énergétique de ce cycle, figé à la fin. Avec Coût pondéré dans le temps activé, la courbe de puissance est facturée au prix en vigueur à chaque instant ; sinon, énergie x le seul prix en vigueur à la fin. Définissez un prix dans les Paramètres pour le renseigner.", "confidence_tip": "Confiance de correspondance du profil (0-100 %). À quel point la courbe de puissance du cycle correspondait au programme identifié.", "health_tip": "Santé du cycle par ML (plus élevé = mieux). Cliquez sur un cycle pour l'inspecter et l'examiner.", - "flags_tip": "Indicateurs de révision, d'anomalie et de source pour le cycle. Survolez une icône pour plus de détails." + "flags_tip": "Indicateurs de révision, d'anomalie et de source pour le cycle. Survolez une icône pour plus de détails.", + "cost_dynamic_tip": "Pondéré dans le temps : facturé au prix en vigueur pendant le cycle (prix effectif {price} {cur}/kWh)." }, "stat": { "avg": "moy.", @@ -1981,9 +2023,9 @@ "finished": "Le cycle a atteint son état final et s'est terminé." }, "store": { + "search_ph2": "Rechercher par marque ou modèle…", "your_model_tip": "C'est l'appareil que vous avez déclaré dans les Paramètres", "your_model": "Le vôtre", - "search_brand_ph": "Rechercher par marque…", "programs_count": "Programmes : {n}", "browse": "Parcourir", "device": "Appareil", @@ -2028,7 +2070,8 @@ "golden": "Retraitement : marquage des cycles de référence", "suggestions": "Retraitement : suggestions", "ml_training": "Retraitement : entraînement ML", - "health": "Retraitement : santé des cycles" + "health": "Retraitement : santé des cycles", + "costs": "Retraitement : coûts énergétiques" }, "pg_sweep": { "optimize": "Optimiser : {param}" diff --git a/custom_components/ha_washdata/translations/panel/hr.json b/custom_components/ha_washdata/translations/panel/hr.json index d6f5e586..41d23697 100644 --- a/custom_components/ha_washdata/translations/panel/hr.json +++ b/custom_components/ha_washdata/translations/panel/hr.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Pri kraju ovaj program utihne na otprilike {mins} min, zatim povuče oko {watts} W tijekom {secs} s i završava. Uočeno u {seen}/{measured} izmjerenih ciklusa: uređaj to ne radi pri svakom pokretanju.", + "quiet_tail": "~{mins} min tišine na kraju", "artifact_tip": "Otkrivene su {n} anomalije (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", "auto": "(automatski otkriveno)", "built_in_tag": "ugrađeni", @@ -77,11 +79,14 @@ "approved": "Odobreno", "awaiting": "Čeka odobrenje", "imported_tip": "Uvezeno iz zajedničke trgovine. Koristi se samo za podudaranje, ne broji se u statistiku.", + "unmatchable": "ne može se podudariti", "not_importable": "nije dostupno", "exists": "postoji", - "backfilled_tip": "Otkriveno u uvezenoj povijesti snage. Utječe samo na podudaranje programa, ne broji se u statistiku." + "backfilled_tip": "Otkriveno u uvezenoj povijesti snage. Utječe samo na podudaranje programa, ne broji se u statistiku.", + "armed": "(primjenjuje se na sljedeći ciklus)" }, "btn": { + "reset_zoom": "Poništi zumiranje", "set_brand_model": "Postavi marku i model", "refresh_catalog": "Osvježi katalog", "add_device": "+ Dodaj uređaj", @@ -217,6 +222,8 @@ "trim": "Obreži", "trim_split": "Obreži / Podijeli", "undo": "Poništi", + "unlabel_selected": "Ukloni oznaku s odabranih ({n})", + "unlabel_selected_tip": "Uklanja oznaku profila s odabranih ciklusa. Ostaju u vašoj povijesti i mogu se kasnije ponovno označiti - samo prestaju oblikovati ovaj profil.", "use": "Koristi", "wipe_all": "Obriši sve podatke", "delete_selected": "Izbriši odabrano ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Primijeni najbolje", "pg_apply_to_settings": "Spremi u postavke", "pg_apply_to_settings_tip": "Kopiraj ovdje uređene vrijednosti u aktivne postavke ovog uređaja", - "search": "Pretraži", "import": "Uvezi", "disconnect": "Odspoji", "connect_github": "Poveži GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Pročitaj iz Home Assistant", "hist_scan": "Traži cikluse", "hist_import_n": "Uvezi cikluse: {n}", - "hist_goto_cycles": "Prikaži cikluse" + "hist_goto_cycles": "Prikaži cikluse", + "save_total_cycles": "Spremi ukupan broj" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Mora biti iznad Praga isključivanja napajanja ({pot} W)", "threshold": "Mora biti ispod Praga zaustavljanja ({stop} W) da bi imalo učinak" }, + "price_entity": { + "power_sensor": "Ovo je Senzor snage uređaja, a ne cijena. Entitet cijene ima prednost pred statičkom cijenom ispod, pa bi se ciklusi obračunavali po vatima za kWh - očistite ovo polje.", + "energy_sensor": "Ovo je Brojilo energije uređaja, a ne cijena. Entitet cijene ima prednost pred statičkom cijenom ispod, pa bi se ciklusi obračunavali po očitanju brojila za kWh - očistite ovo polje.", + "device_class": "Klasa ovog senzora je {dc}, a ne cijena po kWh. Entitet cijene ima prednost pred statičkom cijenom ispod - očistite ovo polje ili ga usmjerite na senzor tarife.", + "unit": "Ovaj senzor očitava u {unit}, što nije cijena po kWh. Entitet cijene ima prednost pred statičkom cijenom ispod - očistite ovo polje ili ga usmjerite na senzor tarife." + }, "pump_stuck": { "duration": "Mora biti manji od Vremenskog ograničenja bez ažuriranja ({to} s) kako bi alarm zaglavljene pumpe okinuo prije nego nadzor prekine ciklus", "timeout": "Mora premašiti Trajanje zaglavljene pumpe ({ps} s) kako bi alarm okinuo prije prisilnog zaustavljanja ciklusa" @@ -349,7 +362,7 @@ "pg_settings_source": "Izvor postavki", "phase_catalog": "Katalog faza", "playground": "Pješčanik", - "process_history": "Povijest procesa", + "process_history": "Obrada povijesti", "settings_history": "Povijest postavki", "status": "Status", "status_graph": "Grafikon stanja", @@ -360,12 +373,12 @@ "wipe_history": "Obriši povijest", "recommendations": "Preporuke ({n})", "pg_detection_params": "Postavke otkrivanja", - "pg_timeline": "Vremenska crta događaja", "pg_outcome": "Ishod simulacije", "pg_across_cycles": "Kroz sve vaše cikluse", "community_store": "Zajednička trgovina", "online_account": "Zajednička trgovina i online značajke", - "import_power_history": "Uvoz povijesti snage" + "import_power_history": "Uvoz povijesti snage", + "service_status": "Status servisa" }, "health": { "fair": "Prihvatljiva kvaliteta profila", @@ -373,6 +386,10 @@ "poor": "⚠ Loša kvaliteta profila" }, "lbl": { + "envelope_position_tip": "Položaj na vlastitoj snimljenoj krivulji podudarenog programa, {pct}%. Mjeri se poravnavanjem ovog ciklusa s profilom umjesto brojanjem vremena, pa ostaje točan kada je pokretanje dulje ili kraće nego obično. Osvježava se dok je uređaj tih, pa tijekom aktivne faze može kasniti.", + "envelope_position": "krivulja {pct}%", + "aria_playground_chart3": "Interaktivni grafikon snage ciklusa: prijeđite mišem za prikaz vremena/snage, pomičite kotačić ili koristite dva prsta za zumiranje, povucite za pomicanje", + "device_unresolved": "Nedostupan uređaj ({id})", "drag_to_resize": "Povucite za promjenu veličine", "actions": "Radnje", "activity": "Aktivnost", @@ -509,7 +526,7 @@ "profile_index": "indeks profila →", "profile_name": "Ime profila", "program": "Program", - "program_tip": "Nadjačajte koji profil se poklapa s trenutnim ciklusom. Automatsko otkrivanje omogućuje integraciji da automatski odabere najbolju podudarnost. Prikvačite određeni program za prisilno podudaranje kada automatsko otkrivanje nije ispravno ili znate što radi.", + "program_tip": "Nadjačajte koji profil se poklapa s trenutnim ciklusom. Automatsko otkrivanje omogućuje integraciji da automatski odabere najbolju podudarnost. Prikvačite određeni program za prisilno podudaranje kada automatsko otkrivanje nije ispravno ili znate što radi. Odaberite program prije pokretanja uređaja i primijenit će se čim započne sljedeći ciklus.", "progress": "Napredak", "raw_socket": "Sirovi signal", "read_only": "Samo za čitanje", @@ -535,7 +552,7 @@ "settings_basic": "Osnovno", "settings_detail_level": "Razina detalja postavki", "settings_overrides": "Zamjene postavki", - "show_debug": "Prikaži karticu za otklanjanje pogrešaka utakmice uživo na stranici statusa (povjerenje, dvosmislenost, najbolji kandidati)", + "show_debug": "Prikaži karticu za otklanjanje pogrešaka podudaranja uživo na stranici statusa (pouzdanost, dvosmislenost, najbolji kandidati)", "show_expected": "Prikaži očekivani sloj krivulje (podudarni profil, narančasto)", "show_raw": "Prikaži prekidač sirovih očitanja utičnice u živom grafu snage", "sparkline": "Nedavni trend trajanja ciklusa", @@ -544,6 +561,7 @@ "stage4": "Faza 4 – podudaranje", "status": "Status", "tail_trim": "Obrezivanje kraja (s)", + "tap_to_dismiss": "dotaknite grafikon za zatvaranje", "timer_auto_pause": "Automatska pauza", "timer_min": "min", "timer_msg_placeholder": "Poruka (nije obavezno, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Preostalo vrijeme (model)", "pg_time_left_model_tip": "Modelom procijenjeno preostalo vrijeme (procjenitelj faza + mješavina s ML), a ne statičko odbrojavanje", "phase": "Faza", - "pg_mode_simulate": "Simuliraj", "pg_mode_history": "Testiraj na povijesti", - "pg_mode_sweep": "Pretraživanje", "pg_ev_detected": "Otkriveno", "pg_ev_match": "Podudaranje potvrđeno", "pg_ev_match_changed": "Podudaranje promijenjeno", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parametar", "pg_objective": "Cilj", "pg_steps": "Koraci", - "pg_sweep_2d": "Toplinska karta 2D", - "pg_sweep_param_y": "2. parametar", "pg_obj_match": "Točnost podudaranja", "pg_obj_endtiming": "Točnost kraja", "pg_obj_falseend": "Stopa lažnih završetaka", "pg_obj_overrun": "Odstupanje trajanja", "pg_obj_ambiguity": "Stopa nejasnoća", - "pg_best_combo": "najbolja kombinacija", "pg_mode_optimize": "Optimiziraj", "pg_group_matching": "Podudaranje programa", "pg_zoom_hint": "dvostruki klik poništava zumiranje", @@ -706,7 +719,12 @@ "task_history_import_apply": "Uvoz ciklusa", "evidence_real_cycles": "Ciklusi koje je izvršio ovaj uređaj", "evidence_reference_cycles": "Preuzeti iz zajedničke trgovine", - "evidence_backfill_cycles": "Nađeni u uvezenoj povijesti snage" + "evidence_backfill_cycles": "Nađeni u uvezenoj povijesti snage", + "zoom_hint_touch": "zumirajte s dva prsta", + "zoom_hint_touch_zoomed": "zumirajte s dva prsta · dvostruki dodir za resetiranje", + "cycles_since_service": "{since} / {total} ciklusa", + "total_cycles_run": "ciklusa ukupno", + "correct_total_cycles": "Ispravi ukupan broj" }, "log": { "all_levels": "Sve razine", @@ -756,7 +774,7 @@ "merge_cycles": "Spoji {n} ciklusa", "new_group": "Nova grupa profila", "new_phase": "Nova faza", - "process_history_msg": "Ponovo pokrenite uparivanje, osvježite prijedloge, trenirajte ML (ako je omogućeno) i preračunajte zdravlje ciklusa za sve pohranjene cikluse. Ovo može potrajati.", + "process_history_msg": "Ponovo pokrenite podudaranje, osvježite prijedloge, ponovo trenirajte ML (ako je omogućeno), preračunajte troškove prema zabilježenim cijenama energije i preračunajte zdravlje ciklusa za sve pohranjene cikluse. Ovo može potrajati.", "process_history_ok": "Obradi", "process_history_title": "Obradi povijest", "process_recording": "Snimanje procesa", @@ -789,6 +807,7 @@ "history_import": "Uvoz povijesti snage" }, "msg": { + "pg_canvas_empty3": "Odaberite ciklus iznad i pritisnite Pokreni da ga simulirate. Zatim prijeđite mišem za prikaz vrijednosti, pomičite kotačić ili koristite dva prsta za zumiranje i povucite za pomicanje.", "tail_trim_hint": "Uklonite ovoliko sekundi s kraja", "store_sibling_hint": "Nema ničega podijeljenog za vaš točan model? Blisko srodan model iste marke obično je dobra početna točka.", "store_declare_appliance": "Recite integraciji WashData koji uređaj posjedujete i ova kartica prikazat će konfiguracije koje su drugi podijelili za njega. Možete i upisati marku iznad kako biste pregledali katalog.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Poraslo iznad uobičajenog pojasa snage ~{n}s.", "auto_label_intro": "Dodijelite profile neoznačenim ciklusima čija pouzdanost podudaranja prelazi prag.", "automations_intro": "WashData pokreće događaje {start} / {end} i izlaže entitete, pa je obavijesti i radnje najbolje graditi kao normalne automatizacije Home Assistanta. Automatizacije koje koriste ovaj uređaj prikazane su ispod.", - "cleanup_intro": "Svaki označeni ciklus prekriven. Označite izvanredne vrijednosti i izbrišite da biste očistili profil.", + "cleanup_intro": "Svi označeni ciklusi prikazani jedan preko drugoga. Označite odstupajuće cikluse, a zatim im uklonite oznaku (ostaju u povijesti, ali više ne oblikuju ovaj profil) ili ih izbrišite.", "clear_debug_hint": "Uklonite pohranjene podatke o otklanjanju pogrešaka da biste oslobodili prostor.", "collecting_data": "Prikupljanje podataka. Još potrebnih ciklusa prije početka finog podešavanja: {need} ({current}/{min}).", "compare_overlay_profiles": "Prekrivajući profili (slabo)", @@ -902,7 +921,7 @@ "preferences_adv": "Postavke", "prefs_personal": "Ovo se odnosi samo na vaš račun Home Assistant.", "font_size_hint": "Povećajte ili smanjite sve u ovoj ploči. Primjenjuje se na vaš račun na ovom uređaju.", - "process_history_hint": "Ponovo pokrenite uparivanje na svim pohranjenim ciklusima, osvježite prijedloge podešavanja, ponovo trenirajte ML modele (ako su omogućeni) i preračunajte zdravlje ciklusa. Pokrenite nakon niza recenzija.", + "process_history_hint": "Ponovo pokrenite podudaranje na svim pohranjenim ciklusima, osvježite prijedloge podešavanja, ponovo trenirajte ML modele (ako su omogućeni), preračunajte troškove prema zabilježenim cijenama energije i preračunajte zdravlje ciklusa. Pokrenite ovo nakon niza pregleda.", "profile_deleted": "Profil izbrisan", "profile_poor_health_detail": "Ciklusi dodijeljeni ovom profilu imaju nedosljedne oblike ili nisku pouzdanost. Razmislite o ponovnoj izradi omotnice ili pregledu označenih ciklusa.", "profiles_intro": "Pritisnite profil za statistiku, faze i čišćenje. Grupirajte gotovo identične programe (isti oblik/trajanje, različita temperatura ili centrifuga) tako da podudaranje pouzdano bira između njih.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "Profil '{name}' ima nisku ocjenu podudaranja - njegovi noviji ciklusi znatno variraju ili se slabo podudaraju. Pregledajte njegove cikluse ili ponovno snimite profil kako bi prepoznavanje i procjene vremena ostali točni.", "advisory_shape_drift": "Profil '{name}' znatno je odstupio od svog izvornog oblika potrošnje snage. Uređaj je možda s vremenom promijenio ponašanje (npr. kamenac, trošenje). Razmislite o ponovnom snimanju ovog profila s novijim ciklusima.", "advisory_shape_drift_corr": "Profil '{name}' znatno je odstupio od svog izvornog oblika potrošnje snage (korelacija {corr}). Uređaj je možda s vremenom promijenio ponašanje (npr. kamenac, trošenje). Razmislite o ponovnom snimanju ovog profila s novijim ciklusima.", + "advisory_unmatchable": "Profil '{name}' nikada se ne može podudariti: iza njega ne stoji nijedan ciklus s podacima o snazi, pa WashData nema s čime usporediti ciklus koji je u tijeku. Označite ciklus ovog programa (ili snimite novi) kako bi se ponovno uključio u podudaranje.", + "advisory_unmatchable_no_duration": "Profil '{name}' nikada se ne može podudariti: ima ciklus s podacima o snazi, ali bez upotrebljivog trajanja, pa WashData nema referentnu duljinu prema kojoj bi se podudaranje mjerilo. Snimite ovaj program ponovno ili postavite njegovo očekivano trajanje kako bi se vratio u podudaranje.", "pg_no_alerts": "U ovom pokretanju nisu otkriveni problemi.", "pg_override_active": "Koriste se vaše uređene postavke umjesto trenutnih", - "pg_history_intro": "Ponovno reproducirajte svoje nedavne cikluse kroz stvarni detektor i mehanizam podudaranja. Najprije uredite postavku otkrivanja u načinu Simuliraj da biste ovdje vidjeli usporedbu prije/poslije.", "pg_history_empty": "Pritisnite Pokreni za reprodukciju svojih ciklusa.", "pg_history_summary": "{detected}/{total} otkriveno · {correct} ispravno podudareno", "pg_sweep_no_metric": "Nedovoljno podataka za ocjenu ovog cilja.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Pretraživanje nije uspjelo.", "hist_scan_expired": "To pretraživanje više nije dostupno. Pokrenite ga ponovno.", "hist_import_failed": "Uvoz nije uspio.", - "imported_history_readonly": "Otkriveno u uvezenoj povijesti snage. Utječe na podudaranje programa, ali se ne broji u vašu statistiku i ne može se obrezati ni podijeliti. Označite ga da mu dodijelite program." + "imported_history_readonly": "Otkriveno u uvezenoj povijesti snage. Utječe na podudaranje programa, ali se ne broji u vašu statistiku i ne može se obrezati ni podijeliti. Označite ga da mu dodijelite program.", + "odometer_intro": "Ovaj ukupan broj samo raste. Brisanje zapisa o ciklusu ga ne mijenja i nastavlja rasti i nakon ograničenja spremljene povijesti, pa podsjetnici za servis ostaju točni.", + "odometer_correct_hint": "Postavite ovo ako je WashData zabilježio ciklus koji se nikada nije dogodio ili ako je uređaj već obavio cikluse prije instaliranja WashData.", + "no_reminders_set": "Još nema postavljenih podsjetnika za servis.", + "toast_program_armed": "Program postavljen za sljedeći ciklus: {program}" }, "phase_desc": { "anti_crease": "Povremena kratka prevrtanja nakon završetka kako bi se smanjile bore.", @@ -1161,6 +1185,10 @@ "label": "Otpuštanje nakon tihe faze sušenja", "doc": "Kada ciklus prijeđe svoje očekivano trajanje, koliko dugo perilica mora ostati tiha (ispod Praga zaustavljanja) prije nego što WashData prestane čekati na završno ispumpavanje vode i završi ciklus. Povećajte ako vaš uređaj ima dugu tihu fazu sušenja prije kasnog završnog ispumpavanja koje se propušta - šire okno omogućuje da naučeno trajanje prati sezonske promjene (hladnija dovodna voda = dulji ciklusi) umjesto da ostane zaključano na staroj prosječnoj vrijednosti. Čekanje samo skraćuje u odnosu na interno ograničenje završnog ispumpavanja od 30 minuta, nikada ga ne produljuje." }, + "anti_crease_finalize_ratio": { + "doc": "Koliko daleko u očekivanom trajanju podudarajućeg programa ciklus mora biti prije nego što WashData smije zaključiti da je tiho prevrtanje bubnja koje vidi završni rep anti-bore i tu završiti ciklus. Očekivano trajanje je prosjek programa, pa kod sušilice čije vrijeme rada sa senzorskim sušenjem ovisi o količini rublja većina pokretanja nikada ne dosegne 98% tog prosjeka, pa rep ostaje stajati do pričuvnog vremenskog ograničenja - često 15 do 40 minuta nakon što je stroj stvarno stao. Kod takve sušilice smanjite ovu vrijednost (npr. 0,75). Perilice ostavite na zadanoj vrijednosti: perilica veći dio ciklusa provede ispod postavke Maks. snaga anti-bora, pa je ovaj udio jedino što razdvaja tihu fazu usred pranja od repa, a njegovo smanjenje može prepoloviti pranje. Ostavite prazno za zadanu vrijednost (0,98). Ovo je druga postavka od Omjera pametnog završetka, koji upravlja zasebnom provjerom ranog završetka.", + "label": "Omjer dovršetka anti-bore" + }, "anti_wrinkle_enabled": { "doc": "Prepoznajte kratke impulse male snage koje sušilica emitira nakon glavne faze zagrijavanja i držite ih vezanima uz završeni ciklus umjesto da ih čitate kao nove cikluse.", "label": "Omogući otkrivanje anti-bora" @@ -1193,6 +1221,10 @@ "doc": "Ciklusi kraći od ovoga odbacuju se kao ciklusi duhova (probne vožnje, otvaranje vrata za dodavanje čarape).", "label": "Min. trajanje ciklusa" }, + "curve_preroll_seconds": { + "doc": "Koliko sekundi očitanja iz prekinutih pokušaja pokretanja WashData smije prenijeti na početak krivulje ciklusa. Krivulja ciklusa inače počinje pokušajem pokretanja koji konačno uspije, pa kod stroja koji nekoliko puta pokuša krenuti prije nego što se smiri - odabir programa, zaključavanje vrata, prvo punjenje vodom - prve minute stvarne aktivnosti nedostaju u svakoj krivulji. Postavite ovu vrijednost malo iznad najduljeg takvog zaleta koji vaš stroj ima (300 s je za većinu više nego dovoljno). 0 isključuje ovu značajku. Imajte na umu da uključivanje pomiče zabilježeni početak ranije, pa ciklusi zabilježeni prije i poslije promjene nose različita trajanja za isti program; naučeni prosjeci će odstupati dok stariji ciklusi ne isteknu.", + "label": "Uvodne sekunde krivulje" + }, "delay_confirm_seconds": { "doc": "Napajanje mora ostati u pojasu pripravnosti toliko dugo prije nego što se uređaj tretira kao uređaj koji čeka na pokretanje, a ne kao da radi.", "label": "Sekunde potvrde" @@ -1230,7 +1262,7 @@ "label": "Broj ponavljanja završetka" }, "energy_price_entity": { - "doc": "Senzor s trenutnom cijenom električne energije po kWh (npr. dinamička tarifa). Ima prednost nad statičnom cijenom ispod. Svaki ciklus zamrzava cijenu na snazi kada završi.", + "doc": "Senzor s trenutnom cijenom struje po kWh (npr. dinamična tarifa). Mora biti cijena, a ne vlastiti entitet snage ili energije utičnice: ima prednost pred statičnom cijenom ispod, pa bi brojilo kWh ovdje obračunalo svaki ciklus po očitanju brojila umjesto po vašoj tarifi. Kada je Vremenski ponderiran trošak uključen, svaki ciklus se naplaćuje po cijeni koja je vrijedila u svakom trenutku njegova rada; inače se na njega zamrzava cijena koja je vrijedila kada je završio.", "label": "Entitet cijene energije" }, "energy_price_static": { @@ -1258,7 +1290,7 @@ "label": "Invertiraj vanjski okidač (okidaj na ISKLJUČENO)" }, "learning_confidence": { - "doc": "Ako je rezultat podudaranja između ovog i pouzdanosti automatske oznake, obavijest s povratnim informacijama traži od vas da potvrdite identificirani program. Ispod ovog rezultata utakmica je previše neizvjesna da bi isplivala na površinu. Mora biti ispod razine pouzdanosti automatske oznake.", + "doc": "Ako je rezultat podudaranja između ovoga i Pouzdanosti za automatsko označavanje, obavijest s povratnim informacijama traži od vas da potvrdite identificirani program. Ispod ovog rezultata podudaranje je previše nesigurno da bi se prikazalo. Mora biti ispod Pouzdanosti za automatsko označavanje.", "label": "Pouzdanost učenja" }, "linked_device": { @@ -1326,7 +1358,7 @@ "label": "Aktiviraj HA događaje za obavijesti" }, "notify_icon": { - "doc": "Izborna mdi ikona za obavijest (npr. mdi:washing-machine). Ostavite prazno za zadanu platformu.", + "doc": "Mala ikona prikazana na obavijesti (npr. mdi:washing-machine). Android je prikazuje u traci stanja; iOS je prikazuje umjesto ikone aplikacije i zahtijeva mobilnu aplikaciju Home Assistant (companion app) 2026.8 ili noviju. Ostavite prazno za zadanu ikonu platforme.", "label": "Ikona obavijesti" }, "notify_live_chronometer": { @@ -1426,7 +1458,7 @@ "label": "Prag podudaranja" }, "profile_unmatch_threshold": { - "doc": "Ako meč uživo u sredini ciklusa padne ispod ovog rezultata, privremena identifikacija se briše. Držite ga malo ispod praga podudaranja tako da kratak pad u sličnosti ne okrene prikaz natrag na neusklađeno.", + "doc": "Ako podudaranje uživo usred ciklusa padne ispod ovog rezultata, privremena identifikacija se briše. Držite ga malo ispod Praga podudaranja kako kratak pad sličnosti ne bi vratio prikaz natrag na stanje bez podudaranja.", "label": "Prag nepodudaranja" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Nekoliko sekundi pumpa može raditi neprekidno prije nego što se označi kao moguće zaglavljena (pokreće događaj zaglavljene pumpe).", "label": "Trajanje zaglavljene pumpe" }, - "running_dead_zone": { - "doc": "Nakon što ciklus započne, padovi snage unutar ovog prozora se zanemaruju. Perilice se pune hladnom vodom (pada blizu 0 W prije zagrijavanja) - bez ove zaštite ta faza punjenja izgleda kao kraj ciklusa. Time se NE preskaču podaci: trag pune snage bilježi se od T=0. Sugestivni mehanizam mjeri stvarni uzorak pokretanja vašeg stroja i automatski ga određuje.", - "label": "Mrtva zona u tijeku" - }, "sampling_interval": { "doc": "Očekivano vrijeme između očitanja senzora - koristi se za određivanje veličine prozora za izglađivanje i ispravno pokretanje odbijanja. Svako ažuriranje senzora bilježi se bez obzira na ovu vrijednost; samo kalibrira nizvodne izračune. Mehanizam za sugestije mjeri stvarni ritam vašeg senzora iz prošlih ciklusa i automatski ga postavlja.", "label": "Interval uzorkovanja" @@ -1568,13 +1596,17 @@ "label": "Interval profila snage", "doc": "Veličina segmenta za atribut senzora power_profile po profilu (polje prosječnih vata po slotu koje koriste vanjski planeri poput EMHASS i tibber_prices). Manji segmenti zadržavaju kratke skokove snage oštrima; veći segmenti zaglađuju oblik. Zadano 15 min. Samo za čitanje; ne utječe na otkrivanje." }, + "notify_live_silent": { + "label": "Tiha ažuriranja uživo", + "doc": "Samo iOS. Napredak uživo osvježavajte tiho: svako ažuriranje stiže bez zvuka i vibracije, s nižim prioritetom koji štedi bateriju i koji iOS može nakratko grupirati. Prvo ažuriranje u ciklusu (ono koje pokreće Live Activity), kao i obavijesti o početku i završetku, i dalje se javljaju kao i obično. Isključite ovo da biste bili obaviješteni pri svakom ažuriranju." + }, "notify_live_sticky": { "label": "Zadrži živu obavijest pri dodiru", "doc": "Samo Android. Živu obavijest o napretku postavite kao trajnu (prikvačenu) kako dodir ne bi odbacio aktivnu nit. Isključeno zadržava zadano ponašanje gdje dodir odbacuje obavijest." }, "notify_live_click_action": { - "label": "Odredište dodira na živoj obavijesti", - "doc": "Samo Android. Gdje dodir na živu obavijest o napretku otvara (npr. /lovelace/laundry ili cijeli URL) umjesto početne stranice aplikacije. Ostavite prazno za zadano." + "label": "Odredište dodira na obavijesti", + "doc": "Kamo vodi dodir obavijesti s ovog uređaja, na sustavima Android i iOS. Prazno polje otvara WashData ploču na ovom uređaju. Unesite putanju nadzorne ploče (npr. /lovelace/laundry) ili cijeli URL da vodi negdje drugdje, ili „none“ ako dodir ne treba ništa otvarati." }, "door_opens_at_end": { "label": "Vrata se automatski otvaraju na kraju", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Ciklusi koji oblikuju program", "doc": "Koji se ciklusi koriste za izgradnju krivulje snage svakog programa i za podudaranje završenog ciklusa s njom. Kada odznačite neku vrstu, ona više ne utječe na vaše programe, ali se ništa ne briše - ciklusi ostaju na popisu Ciklusi i još ih je moguće označiti ili ukloniti. Korisno ako ne vjerujete uvezenim podacima. Na statistiku to ne utječe: uvijek se broje samo ciklusi koje je ovaj uređaj stvarno izvršio. Odznačavanje svega se ignorira jer se program bez ikakvih ciklusa nikada ne bi mogao podudarati." + }, + "energy_price_dynamic": { + "label": "Vremenski ponderiran trošak", + "doc": "Naplaćuje svaki ciklus po cijeni koja je vrijedila u svakom trenutku njegova rada, umjesto po jednoj cijeni koja je vrijedila kada je završio. Odnosi se samo na entitet cijene (statična cijena se ne može mijenjati). Ne treba dodatno postavljanje: cijena se prati uživo dok ciklus traje, a Obrada povijesti u Dijagnostici ponovno obračunava starije cikluse iz recordera. Isključeno = klasično ponašanje, jedna cijena zamrznuta na kraju." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Dijeljenje…", "deleting": "Brisanje…", "imported": "Uvezeno", - "preparing": "Priprema…" + "preparing": "Priprema…", + "unlabelling": "Uklanjanje oznake…" }, "suggestion": { "both_agree": "WashData preporučuje", @@ -1745,10 +1782,6 @@ "higher": "Potvrđuje samo visoko pouzdana podudaranja; više ciklusa ostaje neoznačeno", "lower": "Podudara više ciklusa; neki mogu biti blago pogrešni" }, - "running_dead_zone": { - "higher": "Zanemaruje kratke skokove snage u mirovanju – manje osjetljiv na šum", - "lower": "Reagira na kraće impulse snage – otkriva brzu prolaznu aktivnost" - }, "start_threshold_w": { "higher": "Sprječava lažne startove od kratkih skokova snage", "lower": "Otkriva programe s niskom snagom; može reagirati na šum" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} čistih ciklusa ({pct}%) imalo je unutarnju pauzu dulju od 60s koja se nastavila; zatraži {val} potvrdu/potvrde kraja.", "learning_confidence": "Pouzdanost p05 za {cycles} korisnički označenih ciklusa ({p05}); ispod ove vrijednosti zatraži provjeru.", "match_interval": "Na temelju uočene učestalosti ažuriranja (medijan={median}s) * 10.", + "match_interval_capped": "Ograničeno tako da {persistence} uzastopnih podudaranja stane u {pct}% najkraćeg programa ({shortest}s); sama učestalost ažuriranja (medijan={median}s) predložila bi dulji interval.", + "match_interval_floored": "Već bi i sam najkraći program ({shortest}s) ovu vrijednost ograničio na {cap}s, ispod minimuma od {minimum}s, pa se zadržava na minimumu: niz od {persistence} uzastopnih podudaranja traje {budget}s, što je više od {pct}% tog programa. Podudaranje se pokreće samo kad stigne novo očitanje (medijan={median}s), pa minimum ne donosi dodatno opterećenje.", "max_duration_ratio": "Na temelju trajanja označenih ciklusa (p95={p95}).", "min_active": "Na temelju najmanje radne snage ({min}W) uočene u posljednjem ciklusu.", "min_duration_ratio": "Zadržano što nižim kako bi se program prepoznao rano u ciklusu; pragovi pouzdanosti i dvosmislenosti sprječavaju prijevremene potvrde.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Snimanje zaustavljeno", "review_saved": "Pregled spremljen", "settings_conflicts": "Ispravite istaknute sukobe postavki prije spremanja.", + "settings_no_changes": "Nema promjena za spremanje", "settings_reverted": "Postavke vraćene; ponovno učitavanje integracije", "settings_saved": "Postavke spremljene; ponovno učitavanje integracije", "suggestions_applied": "Prijedlozi primijenjeni; ponovno učitavanje integracije", @@ -1949,7 +1985,11 @@ "export_selective_done": "Izvoz preuzet", "import_selective_done": "Uvezeno {profiles} profila i {cycles} ciklusa", "hist_csv_required": "Najprije učitajte CSV datoteku ili zalijepite njezin sadržaj", - "file_read_failed": "Nije bilo moguće pročitati tu datoteku" + "file_read_failed": "Nije bilo moguće pročitati tu datoteku", + "unlabel_done": "Uklonjena oznaka s {count} ciklus(a)", + "unlabel_failed": "Uklanjanje oznake nije uspjelo: {error}", + "odometer_saved": "Ukupan broj ciklusa spremljen", + "odometer_save_failed": "Nije moguće spremiti ukupan broj: {error}" }, "trend": { "down": "Trend pada", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Perilica posuđa: sekunde tišine nakon očekivanog trajanja prije nego što se otpusti čekanje na završno ispumpavanje vode", "smart_termination_duration_ratio": "Udio očekivanog trajanja podudarajućeg programa koji ciklus mora dosegnuti prije nego što ga Pametni završetak može ranije završiti; smanjite ga za strojeve ovisne o količini rublja ili temperaturi", + "anti_crease_finalize_ratio": "Udio očekivanog trajanja podudarajućeg programa koji ciklus mora dosegnuti prije nego što se završni rep prevrtanja anti-bore smije dovršiti; smanjite ga kod sušilica s dugim repom prevrtanja, perilice ostavite na zadanoj vrijednosti", "anti_wrinkle_enabled": "Upij impulse bubnja nakon glavne faze, a ne čitaj ih kao nove cikluse", "anti_wrinkle_exit_power": "Snaga mora pasti ispod ove vrijednosti između impulsa da bi anti-bora ostala aktivna", "anti_wrinkle_idle_timeout": "Dopušteno vrijeme mirovanja između dva impulsa bubnja prije nego što anti-bora završi", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Faza 3: udaljenost polu-zasićenja DDTW; manja = osjetljivija na oblik (zadano 30)", "dtw_refine_top_n": "Faza 3: kandidati koje DTW ponovo ocjenjuje; povećajte na 7-9 ako ispravni profil zauzme 4.-5. mjesto (zadano 5)", "duration_scale": "Faza 4: omjer logaritma gdje se slaganje trajanja prepolovi; manje = stroža kazna (zadano 0,175)", - "energy_scale": "Faza 4: omjer logaritma gdje se slaganje energije prepolovi; manje = stroža kazna (zadano 0,25)" + "energy_scale": "Faza 4: omjer logaritma gdje se slaganje energije prepolovi; manje = stroža kazna (zadano 0,25)", + "curve_preroll_seconds": "Sekunde očitanja iz prekinutih pokušaja pokretanja prenesene na početak krivulje ciklusa; 0 = isključeno" }, "col": { "profile_tip": "Naziv podudarnog programa. Bez oznake znači da se na kraju ciklusa nijedan profil nije podudarao.", @@ -1998,10 +2040,11 @@ "date_tip": "Datum i vrijeme početka ciklusa.", "duration_tip": "Ukupno vrijeme rada ciklusa od početka do kraja.", "energy_tip": "Ukupna potrošena energija (kWh). Izračunato integracijom snage kroz vrijeme.", - "cost_tip": "Trošak energije za ovaj ciklus, zamrznut po završetku prema tada važećoj cijeni (energija x cijena po kWh). Postavite cijenu u Postavkama da biste ga popunili.", + "cost_tip": "Trošak energije za ovaj ciklus, zamrznut pri završetku. Kada je Vremenski ponderiran trošak uključen, krivulja snage se naplaćuje po cijeni koja vrijedi u svakom trenutku; inače energija x jedna cijena koja vrijedi na kraju. Postavite cijenu u Postavkama da se popuni.", "confidence_tip": "Pouzdanost podudaranja profila (0-100%). Koliko se točno krivulja snage ciklusa podudarala s identificiranim programom.", "health_tip": "Zdravlje ciklusa ML (više = bolje). Kliknite ciklus da biste ga pregledali i ocijenili.", - "flags_tip": "Oznake zajednice i pokazatelji kvalitete podataka" + "flags_tip": "Oznake zajednice i pokazatelji kvalitete podataka", + "cost_dynamic_tip": "Vremenski ponderirano: naplaćeno po cijeni koja je vrijedila tijekom ciklusa (efektivno {price} {cur}/kWh)." }, "stat": { "avg": "pros.", @@ -2028,9 +2071,9 @@ "finished": "Ciklus je dosegao završno stanje i završio." }, "store": { + "search_ph2": "Pretraži po marki ili modelu…", "your_model_tip": "Ovo je uređaj koji ste naveli u Postavkama", "your_model": "Vaš", - "search_brand_ph": "Pretraži po marki…", "programs_count": "Programi: {n}", "browse": "Pregledaj", "device": "Uređaj", @@ -2075,7 +2118,8 @@ "golden": "Ponovna obrada: popunjavanje referentnih", "suggestions": "Ponovna obrada: prijedlozi", "ml_training": "Ponovna obrada: ML obuka", - "health": "Ponovna obrada: zdravlje ciklusa" + "health": "Ponovna obrada: zdravlje ciklusa", + "costs": "Ponovna obrada: troškovi energije" }, "pg_sweep": { "optimize": "Optimizacija: {param}" diff --git a/custom_components/ha_washdata/translations/panel/hu.json b/custom_components/ha_washdata/translations/panel/hu.json index 214f16e6..d35a393f 100644 --- a/custom_components/ha_washdata/translations/panel/hu.json +++ b/custom_components/ha_washdata/translations/panel/hu.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "A vége felé ez a program körülbelül {mins} percre elcsendesedik, majd a befejezés előtt {secs} másodpercig körülbelül {watts} W-ot vesz fel. {measured} mért ciklusból {seen} esetben fordult elő: a készülék nem minden futásnál csinálja.", + "quiet_tail": "záró csend ~{mins} perc", "artifact_tip": "{n} rendellenesség észlelve (pl. ajtónyitás a ciklus közepén) – nyissa meg, hogy megtekinthesse őket a grafikonon", "auto": "(automatikusan észlelve)", "built_in_tag": "beépített", @@ -63,7 +65,7 @@ "needs_review": "Felülvizsgálatra szorul", "overrun": "A szokásosnál tovább futott", "poor_fit": "rossz illeszkedés", - "poor_fit_tip": "Inkonzisztens meccselőzmények – fontolja meg a profil újjáépítését", + "poor_fit_tip": "Inkonzisztens egyezési előzmények – fontolja meg a profil újjáépítését", "restart_gap_tip": "{n} HA-újraindítás-rés: a teljesítménygörbén van egy lyuk", "reviewed": "Áttekintve", "steady": "→ állandó", @@ -77,11 +79,14 @@ "approved": "Jóváhagyva", "awaiting": "Jóváhagyásra vár", "imported_tip": "A közösségi boltból importálva. Csak egyeztetéshez használatos, a statisztikába nem számít bele.", + "unmatchable": "nem egyeztethető", "not_importable": "itt nem elérhető", "exists": "létezik", - "backfilled_tip": "Importált teljesítményelőzményekben észlelve. Csak a programegyeztetést befolyásolja, a statisztikába nem számít bele." + "backfilled_tip": "Importált teljesítményelőzményekben észlelve. Csak a programegyeztetést befolyásolja, a statisztikába nem számít bele.", + "armed": "(a következő ciklusra érvényes)" }, "btn": { + "reset_zoom": "Nagyítás visszaállítása", "set_brand_model": "Márka és modell megadása", "refresh_catalog": "Katalógus frissítése", "add_device": "+ Eszköz hozzáadása", @@ -215,6 +220,8 @@ "trim": "Vágás", "trim_split": "Vágás / Felosztás", "undo": "Visszavonás", + "unlabel_selected": "Kijelöltek címkéjének eltávolítása ({n})", + "unlabel_selected_tip": "Eltávolítja a profilcímkét a kijelölt ciklusokról. A ciklusok az előzményekben maradnak, és később újra címkézhetők - csak már nem alakítják ezt a profilt.", "use": "Használat", "wipe_all": "Törölje az összes adatot", "delete_selected": "Kijelöltek törlése ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Legjobb alkalmazása", "pg_apply_to_settings": "Mentés a beállításokba", "pg_apply_to_settings_tip": "Az itt szerkesztett értékek másolása az eszköz aktív beállításaiba", - "search": "Keresés", "import": "Importálás", "disconnect": "Leválasztás", "connect_github": "GitHub összekapcsolása", @@ -249,7 +255,8 @@ "hist_read_recorder": "Beolvasás a Home Assistantból", "hist_scan": "Ciklusok keresése", "hist_import_n": "{n} ciklus importálása", - "hist_goto_cycles": "Ciklusok megjelenítése" + "hist_goto_cycles": "Ciklusok megjelenítése", + "save_total_cycles": "Összesítés mentése" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Mentés előtt javítsa az ütközéseket", "attn_title": "Beállítási ütközések: {n}", + "price_entity": { + "power_sensor": "Ez a készülék Teljesítmény érzékelője, nem egy ár. Egy ár entitás felülírja az alábbi statikus árat, így a ciklusok költsége úgy számolódna, mintha a wattérték lenne a kWh-nkénti ár - törölje ezt a mezőt.", + "energy_sensor": "Ez a készülék Energiamérője, nem egy ár. Egy ár entitás felülírja az alábbi statikus árat, így a ciklusok költsége úgy számolódna, mintha a mérőóra állása lenne a kWh-nkénti ár - törölje ezt a mezőt.", + "device_class": "Ez az érzékelő {dc} mennyiséget mér, nem kWh-nkénti árat. Egy ár entitás felülírja az alábbi statikus árat - törölje ezt a mezőt, vagy állítsa be egy tarifaérzékelőre.", + "unit": "Ez az érzékelő {unit} egységben ad értéket, ami nem kWh-nkénti ár. Egy ár entitás felülírja az alábbi statikus árat - törölje ezt a mezőt, vagy állítsa be egy tarifaérzékelőre." + }, "settings_banner": "Beállítási ütközések: {n}. Ellenőrizze a kijelölt szakaszokat, és javítsa ki őket mentés előtt.", "settings_banner_btn": "Ugrás az elsőhöz", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Beállítások forrása", "phase_catalog": "Fázis katalógus", "playground": "Homokozó", - "process_history": "Folyamattörténet", + "process_history": "Előzmények feldolgozása", "settings_history": "Beállítások előzményei", "status": "Állapot", "status_graph": "Állapotgrafikon", @@ -360,12 +373,12 @@ "wipe_history": "Törölje az előzményeket", "recommendations": "Javaslatok ({n})", "pg_detection_params": "Érzékelési beállítások", - "pg_timeline": "Események idővonala", "pg_outcome": "Szimuláció eredménye", "pg_across_cycles": "Az összes ciklusban", "community_store": "Közösségi bolt", "online_account": "Közösségi bolt és online funkciók", - "import_power_history": "Teljesítményelőzmények importálása" + "import_power_history": "Teljesítményelőzmények importálása", + "service_status": "Szerviz állapota" }, "health": { "fair": "Elfogadható profilminőség", @@ -373,6 +386,10 @@ "poor": "⚠ Gyenge profilminőség" }, "lbl": { + "envelope_position_tip": "Pozíció az egyező program saját rögzített görbéjén, {pct}%. A ciklus alakjának a profillal való összevetéséből adódik, nem az eltelt idő számolásából, ezért akkor is helyes marad, ha a futás a szokásosnál hosszabb vagy rövidebb. Akkor frissül, amikor a készülék csendes, ezért aktív fázis közben lemaradhat.", + "envelope_position": "görbe {pct}%", + "aria_playground_chart3": "Interaktív ciklusteljesítmény-grafikon: vigye rá a mutatót az idő/teljesítmény leolvasásához, görgessen vagy csippentsen a nagyításhoz, húzza az eltoláshoz", + "device_unresolved": "Nem elérhető eszköz ({id})", "drag_to_resize": "Húzza az átméretezéshez", "pg_anti_wrinkle": "Ránctalanítás", "actions": "Akciók", @@ -508,7 +525,7 @@ "profile_index": "profilindex →", "profile_name": "Profil neve", "program": "Program", - "program_tip": "Felülbírálja, hogy melyik profil illeszkedik az aktuális ciklushoz. Az automatikus észlelés lehetővé teszi, hogy az integráció automatikusan válassza ki a legjobb egyezést. Rögzítsen egy adott programot a kényszer-egyezéshez, ha az automatikus észlelés téves, vagy tudja, mi fut.", + "program_tip": "Felülbírálja, hogy melyik profil illeszkedik az aktuális ciklushoz. Az automatikus észlelés lehetővé teszi, hogy az integráció automatikusan válassza ki a legjobb egyezést. Rögzítsen egy adott programot a kényszer-egyezéshez, ha az automatikus észlelés téves, vagy tudja, mi fut. Válasszon egyet a készülék indítása előtt, és a program a következő ciklus indulásakor érvénybe lép.", "progress": "Előrehaladás", "raw_socket": "Nyers csatlakozó", "read_only": "Csak olvasható", @@ -543,6 +560,7 @@ "stage4": "4. szakasz – egyezés", "status": "Állapot", "tail_trim": "Vágás a végéről (s)", + "tap_to_dismiss": "koppintás a grafikonra a bezáráshoz", "timer_auto_pause": "Automatikus szünet", "timer_min": "min", "timer_msg_placeholder": "Üzenet (nem kötelező, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Hátralévő idő (modell)", "pg_time_left_model_tip": "A modell által becsült hátralévő idő (fázisbecslő + ML keverék), nem statikus visszaszámláló", "phase": "Fázis", - "pg_mode_simulate": "Szimuláció", "pg_mode_history": "Tesztelés előzményeken", - "pg_mode_sweep": "Pásztázás", "pg_ev_detected": "Észlelve", "pg_ev_match": "Egyezés véglegesítve", "pg_ev_match_changed": "Egyezés megváltozott", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Felülbírálja az automatikusan észlelt készenléti teljesítmény alsó értékét (hagyja üresen az automatikus beállításhoz).", "pg_stress_toggle": "Üresjárati leállás tesztelése", "pg_stress_toggle_desc": "Szimulálja, hogy a készülék a felvétel befejezése után a készenléti fogyasztási szintjén marad - megmutatja, hogy és mikor állítja le a WashData a ciklust.", - "pg_sweep_2d": "2D hőtérkép", - "pg_sweep_param_y": "2. paraméter", "pg_obj_match": "Egyezés pontossága", "pg_obj_endtiming": "Befejezési idő pontossága", "pg_obj_falseend": "Téves befejezések aránya", "pg_obj_overrun": "Időtartam eltérése", "pg_obj_ambiguity": "Bizonytalanság aránya", - "pg_best_combo": "legjobb kombináció", "pg_mode_optimize": "Optimalizálás", "pg_group_matching": "Programillesztés", "pg_zoom_hint": "dupla kattintás a nagyítás visszaállításához", @@ -706,7 +719,12 @@ "task_history_import_apply": "Ciklusok importálása", "evidence_real_cycles": "A gép által lefuttatott ciklusok", "evidence_reference_cycles": "Letöltve a közösségi boltból", - "evidence_backfill_cycles": "Importált teljesítményelőzményekben találva" + "evidence_backfill_cycles": "Importált teljesítményelőzményekben találva", + "zoom_hint_touch": "csippentés a nagyításhoz", + "zoom_hint_touch_zoomed": "csippentés a nagyításhoz · dupla koppintás a visszaállításhoz", + "cycles_since_service": "{since} / {total} ciklus", + "total_cycles_run": "lefutott ciklus összesen", + "correct_total_cycles": "Összesített ciklusszám javítása" }, "log": { "all_levels": "Minden szinten", @@ -756,7 +774,7 @@ "merge_cycles": "Egyesítsen {n} ciklust", "new_group": "Új profilcsoport", "new_phase": "Új fázis", - "process_history_msg": "Futtassa újra az egyeztetést, frissítse a javaslatokat, tanítsa újra az ML-t (ha engedélyezve van) és számolja újra a ciklusok egészségét az összes tárolt cikluson. Ez eltarthat egy ideig.", + "process_history_msg": "Futtassa újra az egyeztetést, frissítse a javaslatokat, tanítsa újra az ML-t (ha engedélyezve van), számolja újra a ciklusok költségét a rögzített energiaárak alapján, és számolja újra a ciklusok egészségét az összes tárolt cikluson. Ez eltarthat egy ideig.", "process_history_ok": "Feldolgozás", "process_history_title": "Előzmények feldolgozása", "process_recording": "Folyamat rögzítése", @@ -789,6 +807,7 @@ "history_import": "Teljesítményelőzmények importálása" }, "msg": { + "pg_canvas_empty3": "Válassz ki egy ciklust fent, és nyomd meg a Futtatás gombot a szimulációhoz. Ezután vidd rá a mutatót az értékek leolvasásához, görgess vagy csippents a nagyításhoz, és húzd az eltoláshoz.", "tail_trim_hint": "Ennyi másodperc eltávolítása a végéről", "store_sibling_hint": "Pontosan az Ön modelljéhez nincs semmi megosztva? Ugyanannak a márkának egy közeli modellje általában jó kiindulópont.", "store_declare_appliance": "Adja meg a WashDatának, hogy melyik készülék az Öné, és ez a lap megjeleníti a mások által hozzá megosztott programokat és felvételeket. Fent egy márkát is beírhat, ha csak körül szeretne nézni.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "~{n}s-re a szokásos energiasáv felett volt.", "auto_label_intro": "Rendeljen hozzá profilokat címkézetlen ciklusokhoz, amelyek egyezési megbízhatósága törli a küszöböt.", "automations_intro": "A WashData {start} / {end} eseményeket küld és entitásokat tesz elérhetővé, ezért az értesítések és műveletek legjobban normál Home Assistant automatizálásként építhetők fel. Az ezt az eszközt használó automatizálások alább jelennek meg.", - "cleanup_intro": "Minden címkézett ciklus átfedi. Jelölje be a kiugró értékeket, és törölje a profilt a profil megtisztításához.", + "cleanup_intro": "Minden címkézett ciklus egymásra vetítve. Jelölje be a kiugró értékeket, majd távolítsa el a címkéjüket (az előzményekben maradnak, de már nem alakítják ezt a profilt), vagy törölje őket véglegesen.", "clear_debug_hint": "Távolítsa el a tárolt hibakeresési adatokat, hogy helyet szabadítson fel.", "collecting_data": "Adatgyűjtés folyamatban. A finomhangolás megkezdéséhez még szükséges ciklusok: {need} ({current}/{min}).", "compare_overlay_profiles": "Fedőprofilok (halvány)", @@ -901,7 +920,7 @@ "preferences_admin": "Beállítások, panel és hozzáférés-vezérlés", "preferences_adv": "Preferenciák", "prefs_personal": "Ezek csak a Home Assistant-fiókra vonatkoznak.", - "process_history_hint": "Futtassa újra az egyeztetést az összes tárolt cikluson, frissítse a hangolási javaslatokat, tanítsa újra az ML modelleket (ha engedélyezve van) és számolja újra a ciklusok egészségét. Futtassa ezt egy áttekintési köteg után.", + "process_history_hint": "Futtassa újra az egyeztetést az összes tárolt cikluson, frissítse a hangolási javaslatokat, tanítsa újra az ML-modelleket (ha engedélyezve van), számolja újra a ciklusok költségét a rögzített energiaárak alapján, és számolja újra a ciklusok egészségét. Futtassa ezt egy köteg felülvizsgálat után.", "profile_deleted": "Profil törölve", "profile_poor_health_detail": "Az ehhez a profilhoz rendelt ciklusok alakja inkonzisztens vagy alacsony a megbízhatóságuk. Fontolja meg a boríték újjáépítését vagy a címkézett ciklusok áttekintését.", "profiles_intro": "Kattintson egy profilra a statisztikák, fázisok és tisztítás megtekintéséhez. Csoportosítsa a közel azonos programokat (azonos forma/időtartam, eltérő hőmérséklet vagy centrifugálás), így az illesztés megbízhatóan kiválasztja őket.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "A(z) '{name}' illeszkedési pontszáma alacsony - a közelmúltbeli ciklusai erősen ingadoznak vagy gyengén illeszkednek. Nézze át a ciklusait, vagy rögzítse újra a profilt, hogy a felismerés és az időbecslés pontos maradjon.", "advisory_shape_drift": "A(z) '{name}' jelentősen eltért az eredeti teljesítménygörbéjétől. A készülék működése idővel megváltozhatott (pl. vízkő, kopás). Fontolja meg a profil újrarögzítését a közelmúltbeli ciklusokkal.", "advisory_shape_drift_corr": "A(z) '{name}' jelentősen eltért az eredeti teljesítménygörbéjétől (korreláció: {corr}). A készülék működése idővel megváltozhatott (pl. vízkő, kopás). Fontolja meg a profil újrarögzítését a közelmúltbeli ciklusokkal.", + "advisory_unmatchable": "A(z) '{name}' soha nem egyeztethető: nincs mögötte teljesítményadatokkal rendelkező ciklus, így a WashData nem tud mihez hasonlítani egy futó ciklust. Címkézze fel ennek a programnak egy ciklusát (vagy rögzítsen egyet), hogy a profil visszakerüljön az egyeztetésbe.", + "advisory_unmatchable_no_duration": "A(z) '{name}' soha nem egyeztethető: van mögötte teljesítményadatokkal rendelkező ciklus, de nincs használható időtartama, így a WashDatának nincs mihez méretezni az egyeztetést. Rögzítse újra ezt a programot, vagy adja meg a várható időtartamát, hogy a profil visszakerüljön az egyeztetésbe.", "pg_no_alerts": "Ebben a futásban nem észlelt problémát.", "pg_override_active": "A szerkesztett beállításait használja a jelenlegiekhez képest", - "pg_history_intro": "Játssza le legutóbbi ciklusait a valódi érzékelőn és illesztőn keresztül. Először szerkesszen egy érzékelési beállítást a Szimuláció módban, hogy itt egy előtte/utána összehasonlítást lásson.", "pg_history_empty": "Nyomja meg a Futtatás gombot a ciklusai lejátszásához.", "pg_history_summary": "{detected}/{total} észlelve · {correct} helyesen illesztve", "pg_sweep_no_metric": "Nincs elég adat ennek a célnak az értékeléséhez.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "A vizsgálat nem sikerült.", "hist_scan_expired": "Ez a vizsgálat már nem elérhető. Kérjük, futtassa újra.", "hist_import_failed": "Az importálás nem sikerült.", - "imported_history_readonly": "Importált teljesítményelőzményekben észlelve. Befolyásolja a programegyeztetést, de nem számít bele a statisztikákba, és nem vágható vagy osztható. Címkézze fel a hozzá tartozó program megadásához." + "imported_history_readonly": "Importált teljesítményelőzményekben észlelve. Befolyásolja a programegyeztetést, de nem számít bele a statisztikákba, és nem vágható vagy osztható. Címkézze fel a hozzá tartozó program megadásához.", + "odometer_intro": "Ez az összesítés csak növekedhet. Egy ciklusbejegyzés törlése nem változtatja meg, és a tárolt előzmények korlátján túl is számol tovább, így a szerviz emlékeztetők pontosak maradnak.", + "odometer_correct_hint": "Akkor állítsa be, ha a WashData olyan futást rögzített, amely soha nem történt meg, vagy ha a készülék már a WashData telepítése előtt is futtatott ciklusokat.", + "no_reminders_set": "Még nincs beállítva szerviz emlékeztető.", + "toast_program_armed": "Program beállítva a következő ciklushoz: {program}" }, "phase_desc": { "anti_crease": "Alkalmankénti rövid bukdácsolás a befejezés után a ráncok csökkentése érdekében.", @@ -1230,7 +1254,7 @@ "label": "Záró ismétlések száma" }, "energy_price_entity": { - "doc": "Érzékelő az aktuális elektromos áram kWh-nkénti árával (pl. dinamikus tarifa). Elsőbbséget élvez az alábbi statikus árral szemben. Minden ciklus lefagyasztja az árat, amikor befejeződött.", + "doc": "Érzékelő az aktuális elektromos áram kWh-nkénti árával (pl. dinamikus tarifa). Árnak kell lennie, nem a konnektor saját teljesítmény- vagy energiaentitásának: elsőbbséget élvez az alábbi statikus árral szemben, így egy ide megadott kWh-számláló minden ciklust a mérőóra állása szerint számol el a tarifája helyett. Ha az Időben súlyozott költség be van kapcsolva, minden ciklus azon az áron kerül elszámolásra, amely a futása minden pillanatában érvényes volt; egyébként a befejezéskor érvényes ár rögzül rá.", "label": "Energiaár entitás" }, "energy_price_static": { @@ -1326,7 +1350,7 @@ "label": "HA események aktiválása értesítésekhez" }, "notify_icon": { - "doc": "Opcionális mdi ikon az értesítéshez (pl. mdi:mosógép). Hagyja üresen a platform alapértelmezett beállításához.", + "doc": "Az értesítésen megjelenő kis ikon (pl. mdi:washing-machine). Az Android az állapotsorban rajzolja ki; az iOS az alkalmazás ikonja helyett jeleníti meg, és ehhez a 2026.8 vagy újabb kísérőalkalmazás szükséges. Hagyja üresen a platform alapértelmezett beállításához.", "label": "Értesítés ikonja" }, "notify_live_chronometer": { @@ -1426,7 +1450,7 @@ "label": "Egyezési küszöb" }, "profile_unmatch_threshold": { - "doc": "Ha egy élő ciklus közbeni meccs ez alá csökken, a próbaazonosító törlődik. Tartsa egy kicsit az egyezési küszöb alatt, hogy a hasonlóság rövid csökkenése ne fordítsa vissza páratlanná a kijelzőt.", + "doc": "Ha egy élő, ciklus közbeni egyezés pontszáma ez alá csökken, az ideiglenes azonosítás törlődik. Tartsa kicsivel az Egyezési küszöb alatt, hogy a hasonlóság rövid visszaesése ne állítsa vissza a kijelzőt a nincs egyezés állapotba.", "label": "Nem egyező küszöb" }, "progress_reset_delay": { @@ -1437,10 +1461,6 @@ "doc": "Másodpercekkel a szivattyú folyamatosan működhet, mielőtt esetleg elakadtként jelölik meg (kiváltja a beragadt szivattyú eseményt).", "label": "Elakadt szivattyú időtartama" }, - "running_dead_zone": { - "doc": "A ciklus elindítása után az ablakon belüli teljesítménycsökkenéseket figyelmen kívül hagyja. A mosógépek hideg vízzel töltik fel (a melegítés előtt közel 0 W csökken) – e védelem nélkül a töltési fázis úgy néz ki, mint a ciklus végén. Ez NEM hagyja ki az adatokat: a teljes teljesítmény nyomvonal T=0-tól kerül rögzítésre. A javaslatmotor méri a gép tényleges indítási mintáját, és automatikusan méretezi azt.", - "label": "Futó holttér" - }, "sampling_interval": { "doc": "Várható idő az érzékelő leolvasásai között – a simítóablak méretére és a visszapattanás helyes elindítására szolgál. Ettől az értéktől függetlenül minden érzékelőfrissítés rögzítésre kerül; csak a downstream számításokat kalibrálja. A javaslatmotor méri az érzékelő tényleges ütemét az elmúlt ciklusokból, és automatikusan beállítja ezt.", "label": "Mintavételezési intervallum" @@ -1568,13 +1588,17 @@ "label": "Teljesítményprofil-intervallum", "doc": "A profilonkénti power_profile szenzorattribútum vödörmérete (az EMHASS és tibber_prices, illetve egyéb külső tervezők által felhasznált időköz-alapú átlagos vatt-tömb). A kisebb vödrök megőrzik a rövid teljesítménycsúcsokat; a nagyobbak simítják az alakot. Alapértelmezés: 15 perc. Csak olvasáskor; a felismerést nem befolyásolja." }, + "notify_live_silent": { + "label": "Néma élő frissítések", + "doc": "Csak iOS esetén. Az élő folyamat frissítése csendben történik: minden frissítés hang és rezgés nélkül érkezik, alacsonyabb, akkumulátorkímélő prioritással, amelyet az iOS rövid időre összevonhat. A ciklus első frissítése (amely elindítja az Élő tevékenységet), valamint az indítási és befejezési értesítések továbbra is a megszokott módon jeleznek. Kapcsolja ki, ha minden frissítésről jelzést szeretne kapni." + }, "notify_live_sticky": { "label": "Élő értesítés megtartása koppintás után", "doc": "Csak Android esetén. Az élő folyamatértesítést tartóssá teszi (ragadóssá), hogy rákoppintva ne zárja be a folyamatban lévő szálat. Kikapcsolt állapotban az alapértelmezett viselkedés érvényes, ahol a koppintás elveti az értesítést." }, "notify_live_click_action": { - "label": "Élő értesítés koppintási célhelye", - "doc": "Csak Android esetén. Az alkalmazás kezdőlapja helyett hová nyílik meg az élő folyamatértesítésre való koppintás (pl. /lovelace/laundry, vagy egy teljes URL). Az alapértelmezetthez hagyja üresen." + "label": "Értesítés koppintási célhelye", + "doc": "Hová nyílik meg az ettől a készüléktől érkező értesítésre való koppintás, Androidon és iOS-en egyaránt. Üresen hagyva a WashData panel nyílik meg ezen a készüléken. Adjon meg egy irányítópult-útvonalat (pl. /lovelace/laundry) vagy egy teljes URL-t, ha máshová szeretné irányítani, illetve a \"none\" értéket, ha egyáltalán ne legyen koppintási célhely." }, "door_opens_at_end": { "label": "Az ajtó automatikusan kinyílik a végén", @@ -1587,6 +1611,18 @@ "profile_evidence_sources": { "label": "A programot alakító ciklusok", "doc": "Mely ciklusok szolgálnak az egyes programok teljesítménygörbéjének felépítéséhez, és egy befejezett ciklus ahhoz való egyeztetéséhez. Ha egy fajta jelölését kikapcsolja, az többé nem alakítja a programjait, de semmi nem törlődik - a ciklusok a Ciklusok listában maradnak, és továbbra is címkézhetők vagy eltávolíthatók. Akkor hasznos, ha nem bízik az importált adatokban. A statisztikákra nincs hatással: azok mindig csak azokat a ciklusokat számolják, amelyeket ez a gép valóban lefuttatott. Ha mindent kikapcsol, azt figyelmen kívül hagyjuk, mert egy ciklusok nélküli program soha nem tudna egyezni." + }, + "energy_price_dynamic": { + "label": "Időben súlyozott költség", + "doc": "Minden ciklust azon az áron számol el, amely a futása minden pillanatában érvényes volt, a befejezéskori egyetlen ár helyett. Csak ár entitással működik (a statikus ár nem változik). Nem igényel külön beállítást: az árat a ciklus futása közben élőben követi, a Diagnosztika alatti Előzmények feldolgozása művelet pedig a recorder adataiból számolja újra a régebbi ciklusok költségét. Kikapcsolva = a megszokott működés, egyetlen, a végén rögzített ár." + }, + "anti_crease_finalize_ratio": { + "label": "Ránctalanítás lezárási aránya", + "doc": "Meddig kell eljutnia a ciklusnak az egyeztetett program várható időtartamán belül, mielőtt a WashData eldöntheti, hogy az általa látott alacsony teljesítményű dobforgás a ránctalanítási utószakasz, és ott befejezheti a ciklust. A várható időtartam a program átlaga, ezért egy olyan szárítógépnél, amelynek szenzoros szárítási ideje a töltet méretéhez igazodik, a legtöbb futás soha nem éri el ennek a 98%-át, és az utószakasz a tartalék időtúllépésig kitart, gyakran 15-40 perccel azután, hogy a gép valójában leállt. Ilyen szárítógépen csökkentse ezt az értéket (például 0,75). A mosógépeket hagyja az alapértelmezett értéken: a mosógép a ciklusa nagy részét a Max. ránctalanítási teljesítmény alatt tölti, így egyedül ez a hányad választja el a csendes mosás közbeni szakaszt az utószakasztól, a csökkentése pedig félbevághat egy mosást. Hagyja üresen az alapértelmezéshez (0,98). Ez a beállítás nem azonos az Intelligens leállítási aránnyal, amely egy külön korai befejezési ellenőrzést szabályoz." + }, + "curve_preroll_seconds": { + "label": "Görbe előfutama", + "doc": "Hány másodpercnyi leolvasás kerülhet a megszakadt indítási kísérletekből a ciklus görbéjének elejére. A ciklus görbéje általában azzal az indítási kísérlettel kezdődik, amelyik végül sikerül, ezért egy olyan gépnél, amely néhányszor próbálkozik, mielőtt beállna (programválasztás, ajtózár, első vízbeeresztés), minden görbéből hiányzik a tényleges működés első néhány perce. Állítsa ezt az értéket kicsivel a gépénél előforduló leghosszabb ilyen nekifutás fölé (300 mp a legtöbb géphez bőven elég). A 0 kikapcsolja. Vegye figyelembe, hogy bekapcsolva a rögzített kezdés korábbra kerül, így a változtatás előtt és után rögzített ciklusok ugyanahhoz a programhoz eltérő időtartamot hordoznak; a megtanult átlagok addig csúsznak el, amíg a régebbi ciklusok ki nem esnek." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Felosztás…", "deleting": "Törlés…", "imported": "Importált", - "preparing": "Előkészítés…" + "preparing": "Előkészítés…", + "unlabelling": "Címke eltávolítása…" }, "tab": { "advanced": "Fejlett", @@ -1778,6 +1815,7 @@ "recording_stopped": "A felvétel leállt", "review_saved": "Vélemény mentve", "settings_conflicts": "Mentés előtt javítsa ki a kijelölt beállítási ütközéseket.", + "settings_no_changes": "Nincs mentendő módosítás", "settings_reverted": "A beállítások visszaállítva; integráció újratöltése", "settings_saved": "A beállítások elmentve; integráció újratöltése", "suggestions_applied": "Javaslatok alkalmazva; integráció újratöltése", @@ -1836,7 +1874,11 @@ "export_selective_done": "Export letöltve", "import_selective_done": "{profiles} profil és {cycles} ciklus importálva", "hist_csv_required": "Először töltsön be egy CSV-fájlt, vagy illessze be a tartalmát", - "file_read_failed": "A fájl nem olvasható" + "file_read_failed": "A fájl nem olvasható", + "unlabel_done": "{count} ciklus címkéje eltávolítva", + "unlabel_failed": "A címke eltávolítása sikertelen: {error}", + "odometer_saved": "Összesített ciklusszám mentve", + "odometer_save_failed": "Nem sikerült menteni az összesítést: {error}" }, "suggestion": { "both_agree": "A WashData javasolja", @@ -1883,10 +1925,6 @@ "higher": "Csak nagy magabiztosságú egyezéseket erősít meg; több ciklus marad jelöletlen", "lower": "Több ciklust egyeztet; néhány kissé helytelen lehet" }, - "running_dead_zone": { - "higher": "Figyelmen kívül hagyja a rövid teljesítménycsúcsokat tétlenség közben – zajra kevésbé érzékeny", - "lower": "Rövidebb teljesítménylökésekre reagál – gyors átmeneti aktivitást észlel" - }, "start_threshold_w": { "higher": "Elkerüli a rövid teljesítménycsúcsokból eredő hamis indításokat", "lower": "Kis teljesítményű programokat is érzékel; zajra is aktiválódhat" @@ -1932,7 +1970,9 @@ "thr_batch": "Éppen a p05 legkisebb aktív teljesítmény fölött tartva {cycles} cikluson át ({p05}W), hogy az indítás a lehető legkorábban észlelhető legyen, a leállítási küszöb pedig a gép legkisebb üzemi teljesítménye alatt maradjon.", "tol_per_profile": "A profilonkénti időtartam-szórás p75 értéke {profiles} profilon át ({cycles} ciklus); a stabil profilok nem kapnak büntetést.", "tol_pooled": "{cycles} legutóbbi címkézett ciklus összevont időtartam-szórása alapján (p95 szórás={dev}).", - "watchdog": "A biztonságosan lehető legalacsonyabbra állítva (épp a p95 frissítési szünet, azaz {p95}s felett, és legalább a mintavételezési intervallum ({median}s) kétszerese, min. 30s), hogy az elakadások gyorsan észlelhetők legyenek téves leállítások nélkül." + "watchdog": "A biztonságosan lehető legalacsonyabbra állítva (épp a p95 frissítési szünet, azaz {p95}s felett, és legalább a mintavételezési intervallum ({median}s) kétszerese, min. 30s), hogy az elakadások gyorsan észlelhetők legyenek téves leállítások nélkül.", + "match_interval_capped": "Korlátozva, hogy {persistence} egymást követő egyezés beleférjen a legrövidebb program ({shortest}s) {pct}%-ába; önmagában a frissítési ütem (médián={median}s) hosszabb intervallumot javasolt volna.", + "match_interval_floored": "Önmagában a legrövidebb program ({shortest}s) {cap}s értékre korlátozná ezt, ami a(z) {minimum}s minimum alatt van, ezért a minimumon marad: {persistence} egymást követő egyezés {budget}s időt vesz igénybe, ami a program {pct}%-ánál több. Az egyeztetés csak akkor fut, amikor új mérés érkezik (médián={median}s), így a minimum semmibe sem kerül." }, "exclusions": { "summary": "{total} tévesen észlelt ciklus kizárva: {parts}.", @@ -1990,7 +2030,9 @@ "dtw_ddtw_scale": "3. szakasz: DDTW féltelítési távolság; kisebb = érzékenyebb az alakra (alapértelmezett 30)", "dtw_refine_top_n": "3. szakasz: DTW által újrapontozandó jelöltek; növelje 7-9-re, ha a helyes profil a 4-5. helyen végez (alapértelmezett 5)", "duration_scale": "4. szakasz: logaritmikus arány, ahol az időtartam-egyezés megfeleződik; kisebb = szigorúbb büntetés (alapértelmezett 0.175)", - "energy_scale": "4. szakasz: logaritmikus arány, ahol az energia-egyezés megfeleződik; kisebb = szigorúbb büntetés (alapértelmezett 0.25)" + "energy_scale": "4. szakasz: logaritmikus arány, ahol az energia-egyezés megfeleződik; kisebb = szigorúbb büntetés (alapértelmezett 0.25)", + "anti_crease_finalize_ratio": "Az egyeztetett program várható időtartamának hányada, amelyet a ciklusnak el kell érnie, mielőtt a ránctalanítási dobforgás utószakasza lezárható; hosszú utószakaszú szárítógépeknél csökkentse, a mosógépeket hagyja alapértelmezetten", + "curve_preroll_seconds": "A megszakadt indítási kísérletekből a ciklus görbéjének elejére átvett leolvasások másodpercben; 0 = ki" }, "col": { "profile_tip": "Az egyezést mutató program neve. A címkézetlen azt jelenti, hogy a ciklus végén egyetlen profil sem egyezett.", @@ -1998,10 +2040,11 @@ "date_tip": "A ciklus indulásának dátuma és időpontja.", "duration_tip": "A ciklus teljes futásideje kezdéstől végéig.", "energy_tip": "Az összes elfogyasztott energia (kWh). A teljesítmény idő szerinti integrálásával számítva.", - "cost_tip": "A ciklus energiaköltsége, befejezéskor rögzítve az akkor érvényes árral (energia x kWh-ár). Állíts be egy árat a Beállításokban a kitöltéséhez.", + "cost_tip": "A ciklus energiaköltsége, befejezéskor rögzítve. Ha az Időben súlyozott költség be van kapcsolva, a teljesítménygörbe minden pillanatban az akkor érvényes áron kerül elszámolásra; egyébként energia x a végén érvényes egyetlen ár. Állíts be egy árat a Beállításokban a kitöltéséhez.", "confidence_tip": "A profilegyezés bizalma (0-100%). Mennyire pontosan illeszkedett a ciklus teljesítménygörbéje az azonosított programhoz.", "health_tip": "ML ciklusállapot (magasabb = jobb). Kattints egy ciklusra a vizsgálatához és áttekintéséhez.", - "flags_tip": "Felülvizsgálati, anomália- és forrásjelzők a ciklushoz. Vigye rá a mutatót az ikonra a részletekért." + "flags_tip": "Felülvizsgálati, anomália- és forrásjelzők a ciklushoz. Vigye rá a mutatót az ikonra a részletekért.", + "cost_dynamic_tip": "Időben súlyozott: a ciklus alatt érvényes áron elszámolva (tényleges {price} {cur}/kWh)." }, "stat": { "avg": "átlag", @@ -2028,9 +2071,9 @@ "finished": "A ciklus elérte a végállapotot és befejeződött." }, "store": { + "search_ph2": "Keresés márka vagy típus szerint…", "your_model_tip": "Ez az a készülék, amelyet a Beállításokban megadott", "your_model": "Az Öné", - "search_brand_ph": "Keresés márka szerint…", "programs_count": "Programok: {n}", "browse": "Böngészés", "device": "Készülék", @@ -2075,7 +2118,8 @@ "golden": "Újrafeldolgozás: referenciaciklusok pótlása", "suggestions": "Újrafeldolgozás: javaslatok", "ml_training": "Újrafeldolgozás: ML-tanítás", - "health": "Újrafeldolgozás: ciklusok egészsége" + "health": "Újrafeldolgozás: ciklusok egészsége", + "costs": "Újrafeldolgozás: energiaköltségek" }, "pg_sweep": { "optimize": "Optimalizálás: {param}" diff --git a/custom_components/ha_washdata/translations/panel/is.json b/custom_components/ha_washdata/translations/panel/is.json index 2fccb597..524e5271 100644 --- a/custom_components/ha_washdata/translations/panel/is.json +++ b/custom_components/ha_washdata/translations/panel/is.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Undir lokin verður þetta kerfi kyrrt í um það bil {mins} mín. og dregur svo um {watts} W í {secs} s áður en því lýkur. Sést í {seen} af {measured} mældum lotum: tækið gerir þetta ekki í hverri keyrslu.", + "quiet_tail": "~{mins} mín. kyrr lokaáfangi", "artifact_tip": "{n} frávik fundust (t.d. hurð opnuð í miðjum lotu) – opnaðu til að sjá þau á línuritinu", "auto": "(sjálfvirkt greint)", "built_in_tag": "innbyggður", @@ -77,11 +79,14 @@ "approved": "Samþykkt", "awaiting": "Bíður samþykkis", "imported_tip": "Flutt inn úr samfélagsversluninni. Aðeins notað til samsvörunar, ekki talið með í tölfræði.", + "unmatchable": "fær enga samsvörun", "not_importable": "ekki hér", "exists": "til staðar", - "backfilled_tip": "Greint í innfluttri aflsögu. Hefur aðeins áhrif á kerfissamsvörun, telst ekki með í tölfræði." + "backfilled_tip": "Greint í innfluttri aflsögu. Hefur aðeins áhrif á kerfissamsvörun, telst ekki með í tölfræði.", + "armed": "(gildir fyrir næstu lotu)" }, "btn": { + "reset_zoom": "Endurstilla aðdrátt", "set_brand_model": "Stilla vörumerki og gerð", "refresh_catalog": "Endurnýja skrána", "add_device": "+ Bæta við tæki", @@ -158,7 +163,7 @@ "pg_publish_one": "Vista {label} í samþættinguna", "play": "Spila", "process": "Vinna", - "process_history": "Vinnið núna", + "process_history": "Vinna núna", "process_recording": "Vista upptöku", "process_tip": "Vistaðu skráð sporið sem nýjan eða núverandi prófíl", "rebuild": "Endurbyggja umslög", @@ -215,6 +220,8 @@ "trim": "Klippa til", "trim_split": "Klippa til / Skipta", "undo": "Afturkalla", + "unlabel_selected": "Fjarlægja merkingu ({n})", + "unlabel_selected_tip": "Fjarlægir prófílmerkinguna af völdum lotum. Þær verða áfram í sögunni þinni og hægt er að merkja þær aftur síðar - þær hafa bara ekki lengur áhrif á þennan prófíl.", "use": "Notaðu", "wipe_all": "Þurrkaðu öll gögn", "delete_selected": "Eyða völdum ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Nota besta", "pg_apply_to_settings": "Vista í stillingar", "pg_apply_to_settings_tip": "Afrita gildin sem þú breyttir hér yfir í virkar stillingar þessa tækis", - "search": "Leita", "import": "Flytja inn", "disconnect": "Aftengja", "connect_github": "Tengja GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Lesa úr Home Assistant", "hist_scan": "Leita að lotum", "hist_import_n": "Flytja inn {n} lotur", - "hist_goto_cycles": "Sýndu mér loturnar" + "hist_goto_cycles": "Sýndu mér loturnar", + "save_total_cycles": "Vista samtölu" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Verður að vera yfir Slökkunarþröskuldi ({pot} W)", "threshold": "Verður að vera undir Stöðvunarþröskuldi ({stop} W) til að taka gildi" }, + "price_entity": { + "power_sensor": "Þetta er Aflskynjari tækisins, ekki verð. Verðeining tekur fram yfir fasta verðið hér að neðan, þannig að lotur yrðu reiknaðar á vöttum á kWh - hreinsaðu þennan reit.", + "energy_sensor": "Þetta er Orkumælir tækisins, ekki verð. Verðeining tekur fram yfir fasta verðið hér að neðan, þannig að lotur yrðu reiknaðar á stöðu mælisins á kWh - hreinsaðu þennan reit.", + "device_class": "Þessi skynjari mælir {dc}, ekki verð á kWh. Verðeining tekur fram yfir fasta verðið hér að neðan - hreinsaðu þennan reit eða bentu honum á gjaldskrárskynjara.", + "unit": "Þessi skynjari sýnir gildi í {unit}, sem er ekki verð á kWh. Verðeining tekur fram yfir fasta verðið hér að neðan - hreinsaðu þennan reit eða bentu honum á gjaldskrárskynjara." + }, "pump_stuck": { "duration": "Verður að vera minna en Engin-uppfærsla-tímamark ({to} s) svo fasadæluvísbending kvikni áður en gæslan drepur lotuna", "timeout": "Verður að vera meira en Fastri dælu-lengd ({ps} s) svo fasadæluvísbending kvikni áður en lotunni er þvingað stöðvað" @@ -349,7 +362,7 @@ "pg_settings_source": "Uppruni stillinga", "phase_catalog": "Áfangaskrá", "playground": "Tilraunasvæði", - "process_history": "Ferlissaga", + "process_history": "Vinna úr sögu", "settings_history": "Stillingasaga", "status": "Staða", "status_graph": "Staða graf", @@ -360,12 +373,12 @@ "wipe_history": "Þurrkaðu sögu", "recommendations": "Ráðleggingar ({n})", "pg_detection_params": "Greiningarstillingar", - "pg_timeline": "Tímalína atburða", "pg_outcome": "Niðurstaða hermunar", "pg_across_cycles": "Yfir lotur þínar", "community_store": "Samfélagsverslun", "online_account": "Samfélagsverslun og nettengdir eiginleikar", - "import_power_history": "Flytja inn aflsögu" + "import_power_history": "Flytja inn aflsögu", + "service_status": "Staða þjónustu" }, "health": { "fair": "Viðunanlegt prófíl", @@ -373,6 +386,10 @@ "poor": "⚠ Léleg samsvörun" }, "lbl": { + "envelope_position_tip": "Staðsetning á skráðum ferli kerfisins sem samsvarar, {pct}%. Mælt með því að stilla þessa lotu saman við prófílinn í stað þess að telja tíma, þannig að hún helst rétt þegar keyrsla er lengri eða styttri en venjulega. Hún uppfærist á meðan tækið er kyrrt og getur því dregist aftur úr í virkum áfanga.", + "envelope_position": "ferill {pct}%", + "aria_playground_chart3": "Gagnvirkt aflrit fyrir lotuna: haltu bendlinum yfir til að lesa tíma/afl, skrunaðu eða þysjaðu með tveimur fingrum, dragðu til að hliðra", + "device_unresolved": "Ótiltækt tæki ({id})", "drag_to_resize": "Dragðu til að breyta stærð", "actions": "Aðgerðir", "activity": "Virkni", @@ -513,7 +530,7 @@ "profile_index": "sniðnúmer →", "profile_name": "Nafn prófíls", "program": "Kerfi", - "program_tip": "Hnekktu því sniði sem samsvarar núverandi lotu. Sjálfvirk greining lætur samþættinguna velja bestu samsvörun sjálfkrafa. Festu ákveðið kerfi til að þvinga samsvörun þegar sjálfvirk greining er röng eða þú veist hvað er í gangi.", + "program_tip": "Hnekktu því sniði sem samsvarar núverandi lotu. Sjálfvirk greining lætur samþættinguna velja bestu samsvörun sjálfkrafa. Festu ákveðið kerfi til að þvinga samsvörun þegar sjálfvirk greining er röng eða þú veist hvað er í gangi. Veldu kerfi áður en þú setur tækið í gang og það verður notað um leið og næsta lota byrjar.", "progress": "Framfarir", "raw_socket": "Hrár tengi", "read_only": "Eingöngu lesin", @@ -548,6 +565,7 @@ "stage4": "Þrep 4 – samræmi", "status": "Staða", "tail_trim": "Klippt aftan af (s)", + "tap_to_dismiss": "pikkaðu á ritið til að loka", "timer_auto_pause": "Sjálfvirk hlé", "timer_min": "mín", "timer_msg_placeholder": "Skilaboð (valfrjálst, {device}/{program}/{minutes})", @@ -584,9 +602,7 @@ "pg_time_left_model": "Tími eftir (líkan)", "pg_time_left_model_tip": "Áætlaður tími sem eftir er samkvæmt líkani (fasamatstæki + ML-blanda), ekki föst niðurtalning", "phase": "Fasi", - "pg_mode_simulate": "Herma", "pg_mode_history": "Prófa á sögu", - "pg_mode_sweep": "Yfirferð", "pg_ev_detected": "Greint", "pg_ev_match": "Samsvörun staðfest", "pg_ev_match_changed": "Samsvörun breytt", @@ -616,14 +632,11 @@ "pg_sweep_param": "Færibreyta", "pg_objective": "Markmið", "pg_steps": "Skref", - "pg_sweep_2d": "2D-hitakort", - "pg_sweep_param_y": "2. færibreyta", "pg_obj_match": "Nákvæmni samsvörunar", "pg_obj_endtiming": "Nákvæmni lokatíma", "pg_obj_falseend": "Hlutfall falskra loka", "pg_obj_overrun": "Frávik í lengd", "pg_obj_ambiguity": "Hlutfall óljósra", - "pg_best_combo": "besta samsetning", "pg_mode_optimize": "Fínstilla", "pg_group_matching": "Kerfissamsvörun", "pg_zoom_hint": "tvísmelltu til að núllstilla aðdrátt", @@ -706,7 +719,12 @@ "task_history_import_apply": "Flyt inn lotur", "evidence_real_cycles": "Lotur sem þessi vél hefur keyrt", "evidence_reference_cycles": "Hlaðnar niður úr samfélagsversluninni", - "evidence_backfill_cycles": "Fundnar í innfluttri aflsögu" + "evidence_backfill_cycles": "Fundnar í innfluttri aflsögu", + "zoom_hint_touch": "þysjaðu með tveimur fingrum", + "zoom_hint_touch_zoomed": "þysjaðu með tveimur fingrum · pikkaðu tvisvar til að endurstilla", + "cycles_since_service": "{since} / {total} lotur", + "total_cycles_run": "lotur samtals", + "correct_total_cycles": "Leiðréttu samtöluna" }, "log": { "all_levels": "Öll stig", @@ -756,9 +774,9 @@ "merge_cycles": "Sameina {n} lotur", "new_group": "Nýr prófílflokkur", "new_phase": "Nýr áfangi", - "process_history_msg": "Keyra samsvörun aftur, uppfæra tillögur, þjálfa ML aftur (ef virkt) og endurreikna heilsu lotu yfir allar geymdar lotur. Þetta kann taka smá tíma.", + "process_history_msg": "Keyra samsvörun aftur, uppfæra tillögur, þjálfa ML aftur (ef virkt), endurreikna kostnað lotanna út frá skráðum orkuverðum og endurreikna heilsu lotanna fyrir allar geymdar lotur. Þetta getur tekið nokkra stund.", "process_history_ok": "Keyra", - "process_history_title": "Ferli Sögu", + "process_history_title": "Vinna úr sögu", "process_recording": "Vinna upptöku", "delete_maintenance_msg": "Eyða þessari viðhaldsfærslu? Þetta er ekki hægt að afturkalla.", "delete_maintenance_title": "Eyða viðhaldsatburði", @@ -789,6 +807,7 @@ "history_import": "Flytja inn aflsögu" }, "msg": { + "pg_canvas_empty3": "Veldu lotu að ofan og ýttu á Keyra til að herma hana. Haltu svo bendlinum yfir til að lesa gildi, skrunaðu eða þysjaðu með tveimur fingrum og dragðu til að hliðra.", "tail_trim_hint": "Fjarlægðu þennan fjölda sekúndna frá lokum", "store_sibling_hint": "Er engu deilt fyrir nákvæmlega þína gerð? Náskyld gerð frá sama vörumerki er oftast góður upphafspunktur.", "store_declare_appliance": "Segðu WashData hvaða tæki þú átt, þá sýnir þessi flipi þær uppsetningar sem aðrir hafa deilt fyrir það. Þú getur líka slegið inn vörumerki hér fyrir ofan til að skoða þig um.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Fór yfir venjulegt aflband í ~{n}s.", "auto_label_intro": "Úthlutaðu sniðum á ómerktar lotur þar sem samsvörun þeirra hreinsar þröskuldinn.", "automations_intro": "WashData sendir {start} / {end} atburði og birtir einingar, þannig að tilkynningar og aðgerðir eru best smíðaðar sem venjulegar Home Assistant sjálfvirkingar. Sjálfvirkingar sem nota þetta tæki birtast hér að neðan.", - "cleanup_intro": "Sérhver merkt hringrás lögð yfir. Merktu við frávik og eyddu til að hreinsa upp prófílinn.", + "cleanup_intro": "Allar merktar lotur lagðar hver yfir aðra. Merktu við frávik og fjarlægðu síðan merkingu þeirra (þær haldast í sögunni en hafa ekki lengur áhrif á þennan prófíl) eða eyddu þeim alveg.", "clear_debug_hint": "Fjarlægðu vistuð villuleitargögn til að losa um pláss.", "collecting_data": "Safnar gögnum. Lotur sem vantar áður en fínstilling getur hafist: {need} ({current}/{min}).", "compare_overlay_profiles": "Yfirlagssnið (dauft)", @@ -901,7 +920,7 @@ "preferences_admin": "Óskir, pallborð og aðgangsstýring", "preferences_adv": "Óskir", "prefs_personal": "Þetta á aðeins við um Home Assistant reikninginn þinn.", - "process_history_hint": "Keyra samsvörun aftur á öllum geymdum lotum, uppfæra fínstillingartillögur, þjálfa ML módel aftur (ef virkt) og endurreikna heilsu lotu. Keyrðu þetta eftir hóp yfirfarna.", + "process_history_hint": "Keyra samsvörun aftur á öllum geymdum lotum, uppfæra fínstillingartillögur, þjálfa ML líkönin aftur (ef virkt), endurreikna kostnað lotanna út frá skráðum orkuverðum og endurreikna heilsu lotanna. Keyrðu þetta eftir hóp yfirferða.", "profile_deleted": "Sniði eytt", "profile_poor_health_detail": "Hringrásir sem eru úthlutaðar á þetta snið hafa ósamræmda lögun eða lítið öryggi. Íhugaðu að endurbyggja umslagið eða endurskoða merktar lotur.", "profiles_intro": "Smelltu á prófíl fyrir tölfræði, áfanga og hreinsun. Flokkaðu næstum eins kerfi (sama lögun/lengd, mismunandi hitastig eða snúningur) þannig að samsvörun velji áreiðanlega á milli þeirra.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' er með lágt samsvörunargildi - nýlegar lotur þess eru mjög breytilegar eða samsvara illa. Farðu yfir lotur þess eða taktu sniðið upp aftur svo að greining og tímamat haldist nákvæm.", "advisory_shape_drift": "'{name}' hefur breyst verulega frá upprunalegri aflkúrfu sinni. Tækið kann að hafa breytt hegðun sinni með tímanum (t.d. kalk, slit). Íhugaðu að taka þetta snið upp aftur með nýlegum lotum.", "advisory_shape_drift_corr": "'{name}' hefur breyst verulega frá upprunalegri aflkúrfu sinni (fylgni {corr}). Tækið kann að hafa breytt hegðun sinni með tímanum (t.d. kalk, slit). Íhugaðu að taka þetta snið upp aftur með nýlegum lotum.", + "advisory_unmatchable": "'{name}' getur aldrei fengið samsvörun: engin lota með aflgögnum liggur að baki, svo WashData hefur ekkert til að bera yfirstandandi lotu saman við. Merktu lotu af þessu kerfi (eða taktu eina upp) til að koma því aftur inn í samsvörunina.", + "advisory_unmatchable_no_duration": "'{name}' getur aldrei fengið samsvörun: lotan að baki hefur aflgögn en enga nothæfa lengd, svo WashData hefur enga tímalengd til að miða samsvörun við. Taktu þetta kerfi upp aftur, eða stilltu áætlaða lengd þess, til að koma því aftur inn í samsvörunina.", "pg_no_alerts": "Engin vandamál greindust í þessari keyrslu.", "pg_override_active": "Notar breyttu stillingarnar þínar á móti núverandi", - "pg_history_intro": "Spilaðu nýlegar lotur þínar aftur í gegnum raunverulega greiningu og samsvörun. Breyttu greiningarstillingu í Herma fyrst til að sjá fyrir/eftir samanburð hér.", "pg_history_empty": "Ýttu á Keyra til að spila lotur þínar aftur.", "pg_history_summary": "{detected}/{total} greindar · {correct} rétt samsvaraðar", "pg_sweep_no_metric": "Ekki nóg af gögnum til að meta þetta markmið.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Skönnun mistókst.", "hist_scan_expired": "Sú skönnun er ekki lengur tiltæk. Skannaðu aftur.", "hist_import_failed": "Innflutningur mistókst.", - "imported_history_readonly": "Greint í innfluttri aflsögu. Hún hefur áhrif á kerfissamsvörun en telst ekki með í tölfræðinni þinni og ekki er hægt að klippa hana til eða skipta henni. Merktu hana til að gefa kerfinu heiti." + "imported_history_readonly": "Greint í innfluttri aflsögu. Hún hefur áhrif á kerfissamsvörun en telst ekki með í tölfræðinni þinni og ekki er hægt að klippa hana til eða skipta henni. Merktu hana til að gefa kerfinu heiti.", + "odometer_intro": "Þessi samtala getur aðeins hækkað. Það að eyða lotuskrá breytir henni ekki, og hún heldur áfram að telja fram yfir hámark vistaðrar sögu, svo að þjónustuáminningar haldist réttar.", + "odometer_correct_hint": "Stilltu þetta ef WashData skráði lotu sem aldrei fór fram, eða ef tækið hafði þegar keyrt lotur áður en WashData var sett upp.", + "no_reminders_set": "Engar þjónustuáminningar hafa enn verið stilltar.", + "toast_program_armed": "Kerfi stillt fyrir næstu lotu: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Uppþvottavél: hljóðlátar sekúndur eftir áætlaða lengd áður en biðin eftir útdælingu í lok lotu er losuð", @@ -1093,7 +1117,9 @@ "anti_wrinkle_max_power": "Púls yfir þessu slekkur á hrukkuleysi og opnar nýja lotu", "anti_wrinkle_enabled": "Fangar veltipúlsana eftir aðalfasann í stað þess að lesa þá sem nýjar lotur", "anti_wrinkle_max_duration": "Púls sem er lengri en þetta slekkur á hrukkuleysi og opnar nýja lotu", - "anti_wrinkle_exit_power": "Aflið verður að falla undir þetta á milli púlsa til að hrukkuleysi haldist virkt" + "anti_wrinkle_exit_power": "Aflið verður að falla undir þetta á milli púlsa til að hrukkuleysi haldist virkt", + "anti_crease_finalize_ratio": "Hlutfall af væntanlegri lengd samsvaraða kerfisins sem lota þarf að ná áður en hrukkuvarnarhalanum má ljúka; lækkaðu það fyrir þurrkara með langan hala, skildu þvottavélar eftir á sjálfgefna gildinu", + "curve_preroll_seconds": "Sekúndur af mælingum úr misheppnuðum ræsitilraunum sem teknar eru með fremst í ferli lotu; 0 = slökkt" }, "phase_desc": { "anti_crease": "Stundum hrynur stutt eftir að því er lokið til að draga úr hrukkum.", @@ -1191,6 +1217,10 @@ "label": "Losun eftir hljóðláta þurrkun", "doc": "Þegar lotan fer fram úr áætlaðri lengd sinni: hversu lengi uppþvottavélin þarf að vera hljóðlát (undir Stöðvunarþröskuldi) áður en WashData hættir að bíða eftir lokaútdælingu og lýkur lotunni. Hækkaðu gildið ef vélin þín er með langan hljóðlátan þurrkfasa á undan seinni lokaútdælingu sem sleppur fram hjá - breiðari gluggi leyfir lærðu lengdinni að fylgja árstíðabundnum breytingum (kaldara innrennslisvatn = lengri lotur) í stað þess að festast við gamla meðaltalið. Það styttir aðeins biðtímann miðað við innra 30 mínútna hámark fyrir lokatoppinn, lengir hann aldrei." }, + "anti_crease_finalize_ratio": { + "label": "Hlutfall fyrir lok hrukkuvarnar", + "doc": "Hversu langt inn í væntanlega lengd samsvaraða kerfisins lota þarf að vera komin áður en WashData má ákveða að lágaflsveltan sem hún sér sé hrukkuvarnarhalinn og ljúka lotunni þar. Væntanlega lengdin er meðaltal kerfisins, þannig að í þurrkara þar sem keyrslutími með rakaskynjara ræðst af magni þvottarins ná flestar keyrslur aldrei 98% af henni og halinn situr þess í stað þar til vara-tímamörkin renna út - oft 15 til 40 mínútum eftir að vélin stöðvaðist í raun. Lækkaðu þetta gildi (t.d. 0,75) í slíkum þurrkara. Skildu þvottavélar eftir á sjálfgefna gildinu: þvottavél er undir Hám. hrukkuleys-afli mestallan sinn tíma, þannig að þetta hlutfall er það eina sem skilur rólegan áfanga í miðjum þvotti frá halanum, og lækkun getur helmingað þvott. Skildu eftir autt fyrir sjálfgefið gildi (0,98). Þetta er önnur stilling en Hlutfall snjallrar lokunar, sem stýrir sjálfstæðri athugun á snemmbúinni lokun." + }, "anti_wrinkle_enabled": { "doc": "Viðurkenndu stuttu lág-afls þurrkarapúlsana sem þurrkari gefur frá sér eftir aðalhitafasann og haltu þeim tengdum við lokið lotu í stað þess að lesa þá sem nýjar lotur.", "label": "Virkja hrukkuleysi-greiningu" @@ -1223,6 +1253,10 @@ "doc": "Hringrásum sem eru styttri en þetta er fleygt sem draugalotum (prófun, opnun á hurðinni til að bæta við sokk).", "label": "Lágm. lotulengd" }, + "curve_preroll_seconds": { + "label": "Aðdragandi ferils", + "doc": "Hversu margar sekúndur af mælingum úr misheppnuðum ræsitilraunum WashData má taka með fremst í ferli lotu. Ferill lotu byrjar venjulega á þeirri ræsitilraun sem loks tekst, þannig að í vél sem þreifar sig áfram nokkrum sinnum áður en hún fer almennilega í gang - kerfisval, hurðarlæsing, fyrsta vatnsinntak - vantar fyrstu mínúturnar af raunverulegri virkni í hvern feril. Stilltu þetta aðeins hærra en lengsti slíkur aðdragandi vélarinnar þinnar (300 s duga flestum). 0 slekkur á þessu. Athugaðu að þegar kveikt er á því færist skráð upphaf framar, þannig að lotur fyrir og eftir breytinguna bera ólíka lengd fyrir sama kerfi; lærðu meðaltölin skekkjast þar til eldri loturnar detta út." + }, "delay_confirm_seconds": { "doc": "Rafmagn verður að vera í biðstöðu í þetta lengi áður en heimilistækið er meðhöndlað sem bið eftir að ræsa sig frekar en í gangi.", "label": "Staðfestingarsekúndur" @@ -1260,7 +1294,7 @@ "label": "Fjöldi lokakorna" }, "energy_price_entity": { - "doc": "Skynjari með núverandi raforkuverði á kWst (t.d. kraftmikla gjaldskrá). Tekur fram yfir fasta verðið hér að neðan. Hver lota frystir verðið sem er í gildi þegar henni lýkur.", + "doc": "Skynjari með núverandi raforkuverði á kWh (t.d. breytileg gjaldskrá). Verður að vera verð, ekki aflseining eða orkueining innstungunnar sjálfrar: hann tekur fram yfir fasta verðið hér að neðan, þannig að kWh-teljari hér reiknar hverja lotu á stöðu mælisins í stað gjaldskrárinnar þinnar. Þegar Tímaveginn kostnaður er kveiktur er hver lota reiknuð á því verði sem gilti á hverri stundu meðan hún keyrði; annars er verðið sem gilti þegar henni lauk fryst á hana.", "label": "Orkuverðeining" }, "energy_price_static": { @@ -1356,7 +1390,7 @@ "label": "Kveikja HA viðburði fyrir tilkynningar" }, "notify_icon": { - "doc": "Valfrjálst mdi tákn fyrir tilkynninguna (t.d. mdi: þvottavél). Skildu eftir autt fyrir sjálfgefna pallinn.", + "doc": "Litla táknið sem birtist á tilkynningunni (t.d. mdi:washing-machine). Android birtir það í stöðustikunni; iOS sýnir það í stað táknmyndar forritsins og krefst fylgiforritsins 2026.8 eða nýrra. Skildu eftir autt fyrir sjálfgefið tákn kerfisins.", "label": "Tilkynningatákn" }, "notify_live_chronometer": { @@ -1456,7 +1490,7 @@ "label": "Samsvarþröskuldur" }, "profile_unmatch_threshold": { - "doc": "Ef bein leik á miðjum leik fer niður fyrir þetta stig er bráðabirgðaauðkenningin hreinsuð. Haltu því aðeins undir samsvörunarþröskuldinum svo stutt dýfa í líkingu snúi ekki skjánum aftur í ósamþykkt.", + "doc": "Ef bein samsvörun á miðri lotu fer niður fyrir þetta skor er bráðabirgðaauðkenningin hreinsuð. Haltu því aðeins undir samsvörunarþröskuldinum svo stutt dýfa í líkindum snúi skjánum ekki aftur í „Engin samsvörun“.", "label": "Ósamsvarþröskuldur" }, "progress_reset_delay": { @@ -1467,10 +1501,6 @@ "doc": "Sekúndur sem dæla gæti keyrt samfellt áður en hún er merkt sem hugsanlega föst (kveikir dælutilvikinu sem festist).", "label": "Fastri dælu-lengd" }, - "running_dead_zone": { - "doc": "Eftir að hringrás er hafin, er hunsað að draga úr krafti innan þessa glugga. Þvottavélar fyllast af köldu vatni (lækkar nálægt 0 W fyrir upphitun) - án þessarar verndar lítur áfyllingarfasinn út eins og lok hringrásar. Þetta sleppir EKKI gögnum: fullur kraftur er skráður frá T=0. Tillöguvélin mælir raunverulegt gangmynstur vélarinnar þinnar og stærðir þetta sjálfkrafa.", - "label": "Dauðbelt í gangi" - }, "sampling_interval": { "doc": "Væntanlegur tími á milli skynjaralestra - notaður til að stærð sléttunargluggans og hefja frákast á réttan hátt. Sérhver skynjarauppfærsla er tekin óháð þessu gildi; það kvarðar aðeins niðurstreymisútreikningana. Tillöguvélin mælir raunverulegt hraðafall skynjarans frá fyrri lotum og stillir þetta sjálfkrafa.", "label": "Sýnitakabil" @@ -1598,13 +1628,17 @@ "label": "Aflaprófíl-bil", "doc": "Fötustærð fyrir eigindið power_profile á prófílskynjaraskynjara (flatt fylki meðalwattsgilda á hólf sem notast af ytri skipuleggjendum eins og EMHASS og tibber_prices). Minni fötur halda stuttum aflgöllum skörpum; stærri fötur jafna út ferilinn. Sjálfgefið 15 mín. Eingöngu til birtingar; hefur engin áhrif á greiningu." }, + "notify_live_silent": { + "label": "Hljóðlausar lifandi uppfærslur", + "doc": "Eingöngu iOS. Uppfærir lifandi framvindu hljóðlaust: hver uppfærsla berst án hljóðs eða titrings, með lægri forgangi sem sparar rafhlöðu og iOS getur safnað saman í stutta stund. Fyrsta uppfærsla hringrásar (sú sem ræsir Live Activity) og upphafs- og lokatilkynningarnar láta áfram vita eins og venjulega. Slökktu á þessu til að fá tilkynningu við hverja uppfærslu." + }, "notify_live_sticky": { "label": "Halda lifandi tilkynningu við snertingu", "doc": "Eingöngu Android. Gerir lifandi framvindutilkynningu viðvarandi (sticky) þannig að snertingar lokar henni ekki. Slökkt: sjálfgefin hegðun þar sem snertingar lokar tilkynningu." }, "notify_live_click_action": { - "label": "Snertimarkmið lifandi tilkynningar", - "doc": "Eingöngu Android. Hvert snertingar á lifandi framvindutilkynningu opnar (t.d. /lovelace/laundry eða fulla vefslóð) í stað sjálfgefinna ræsiglugga forritsins. Skildu eftir autt fyrir sjálfgefið." + "label": "Snertimarkmið tilkynningar", + "doc": "Hvert snerting á tilkynningu frá þessu tæki opnar, á Android og iOS. Autt opnar WashData pallborðið á þessu tæki. Sláðu inn slóð á mælaborð (t.d. /lovelace/laundry) eða fulla URL-slóð til að senda það annað, eða „none“ fyrir ekkert snertimarkmið." }, "door_opens_at_end": { "label": "Hurð opnast sjálfkrafa í lok", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Lotur sem móta kerfi", "doc": "Hvaða lotur eru notaðar til að byggja upp aflferil hvers kerfis og til að finna samsvörun við hann fyrir lokna lotu. Ef hakið er tekið af einni gerð hefur hún ekki lengur áhrif á kerfin þín, án þess að neinu sé eytt - loturnar verða áfram í Hringrásir-listanum þínum og enn má merkja þær eða fjarlægja. Gagnlegt ef þú treystir ekki innfluttum gögnum. Tölfræðin verður óbreytt: hún telur alltaf aðeins þær lotur sem þessi vél hefur í raun keyrt. Sé hakið tekið af öllu er það hunsað, því kerfi sem hefur engar lotur að baki gæti aldrei fundið samsvörun." + }, + "energy_price_dynamic": { + "label": "Tímaveginn kostnaður", + "doc": "Reiknar kostnað hverrar lotu á því verði sem gilti á hverri stundu meðan hún keyrði, í stað eins verðs sem gilti þegar henni lauk. Á aðeins við um verðeiningu (fast verð getur ekki breyst). Þarf enga auka uppsetningu: verðinu er fylgt í rauntíma meðan lotan keyrir, og „Vinna úr sögu“ undir Greiningu endurreiknar kostnað eldri lota út frá recorder. Af = klassíska hegðunin, eitt verð fryst við lokin." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Skipti…", "deleting": "Eyði…", "imported": "Flutt inn", - "preparing": "Undirbý…" + "preparing": "Undirbý…", + "unlabelling": "Fjarlægi merkingu…" }, "suggestion": { "both_agree": "WashData mælir með", @@ -1775,10 +1814,6 @@ "higher": "Aðeins mjög öruggar samsvörunir staðfestar – fleiri lotur vera ómerktar", "lower": "Fleiri lotur samsvara – sumar kunna að vera lítið eitt rangar" }, - "running_dead_zone": { - "higher": "Stuttar aflhækkingar í aðgerðaleysi hunsar – minna næmt fyrir hávaða", - "lower": "Bregst við styttri aflboðum – skráir skjótar sveiflur" - }, "start_threshold_w": { "higher": "Forðast rangar ræsingar vegna stuttra aflhækkinga", "lower": "Þekkir kerfi með lágt afl; getur kviknað af hávaða" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} hreinar lotur ({pct}%) höfðu innri hlé lengra en 60s sem hélt svo áfram; krefst {val} staðfestinga á lokum.", "learning_confidence": "p05-vissa fyrir {cycles} lotur sem notandi merkti ({p05}); undir þessu er beðið um staðfestingu.", "match_interval": "Byggt á mældri uppfærslutíðni (miðgildi={median}s) * 10.", + "match_interval_capped": "Takmarkað þannig að {persistence} samfelldar samsvaranir rúmist innan {pct}% af stysta kerfinu ({shortest}s); uppfærslutíðnin ein og sér (miðgildi={median}s) hefði stungið upp á lengra bili.", + "match_interval_floored": "Stysta kerfið ({shortest}s) eitt og sér myndi takmarka þetta við {cap}s, undir {minimum}s lágmarkinu, svo það helst þar: {persistence} samfelldar samsvaranir taka {budget}s, sem er meira en {pct}% af því kerfi. Samsvörun keyrir aðeins þegar mæling berst (miðgildi={median}s), svo lágmarkið kostar ekkert.", "max_duration_ratio": "Byggt á lengd merktra lota (p95={p95}).", "min_active": "Byggt á lægsta virka afli ({min}W) sem mældist í síðustu lotu.", "min_duration_ratio": "Haldið eins lágu og hægt er svo að kerfi greinist snemma í lotunni; vissu- og tvíræðnimörkin koma í veg fyrir ótímabærar ákvarðanir.", @@ -1921,6 +1958,7 @@ "recording_stopped": "Upptaka hætt", "review_saved": "Umsögn vistuð", "settings_conflicts": "Lagðu merktu stillingarárekstrana áður en þú vistar.", + "settings_no_changes": "Engar breytingar til að vista", "settings_reverted": "Stillingar endurheimtar; endurhleðsla samþættingar", "settings_saved": "Stillingar vistaðar; endurhleðsla samþættingar", "suggestions_applied": "Tillögur beitt; endurhleðsla samþættingar", @@ -1979,7 +2017,11 @@ "export_selective_done": "Útflutningur sóttur", "import_selective_done": "Flutti inn {profiles} prófíl(a) og {cycles} lotu(r)", "hist_csv_required": "Hlaðdu fyrst inn CSV-skrá eða límdu innihald hennar", - "file_read_failed": "Gat ekki lesið þá skrá" + "file_read_failed": "Gat ekki lesið þá skrá", + "unlabel_done": "Fjarlægði merkingu af {count} lotu(m)", + "unlabel_failed": "Fjarlæging merkingar mistókst: {error}", + "odometer_saved": "Samtala lotna vistuð", + "odometer_save_failed": "Ekki tókst að vista samtöluna: {error}" }, "trend": { "down": "Stefnir niður", @@ -1998,10 +2040,11 @@ "date_tip": "Dagsetning og tími þegar lotan hófst.", "duration_tip": "Heildarkeyrslutími lotu frá upphafi til enda.", "energy_tip": "Heildarorka sem notuð er (kWh). Reiknuð með því að heilda afl yfir tíma.", - "cost_tip": "Orkukostnaður fyrir þessa lotu, fastsettur við lok út frá verðinu sem var í gildi þá (orka x verð á kWh). Stilltu verð undir Stillingar til að fylla það út.", + "cost_tip": "Orkukostnaður þessarar lotu, frystur við lok hennar. Þegar Tímaveginn kostnaður er kveiktur er aflferillinn reiknaður á því verði sem gilti á hverri stundu, annars orka x eina verðið sem gilti í lokin. Stilltu verð í Stillingum til að fylla þetta út.", "confidence_tip": "Samsvörunaröryggi prófíls (0-100%). Hversu vel aflferill lotu samsvaraði auðkenndu kerfi.", "health_tip": "ML-heilsa lotu (hærra = betra). Smelltu á lotu til að skoða og fara yfir hana.", - "flags_tip": "Yfirferðar-, frávik- og upprunaflagg fyrir lotuna. Haltu músarbendlinum yfir tákn til að sjá upplýsingar." + "flags_tip": "Yfirferðar-, frávik- og upprunaflagg fyrir lotuna. Haltu músarbendlinum yfir tákn til að sjá upplýsingar.", + "cost_dynamic_tip": "Tímavegið: reiknað á því verði sem gilti meðan lotan keyrði (raunverð {price} {cur}/kWh)." }, "stat": { "avg": "meðalt.", @@ -2028,9 +2071,9 @@ "finished": "Lotan náði lokastöðu og endaði." }, "store": { + "search_ph2": "Leita eftir vörumerki eða gerð…", "your_model_tip": "Þetta er tækið sem þú tilgreindir í Stillingum", "your_model": "Þitt tæki", - "search_brand_ph": "Leita eftir vörumerki…", "programs_count": "Kerfi: {n}", "browse": "Skoða", "device": "Tæki", @@ -2076,7 +2119,8 @@ "golden": "Endurvinnsla: fylli inn viðmiðunarlotur", "suggestions": "Endurvinnsla: tillögur", "ml_training": "Endurvinnsla: ML-þjálfun", - "health": "Endurvinnsla: heilsa lotu" + "health": "Endurvinnsla: heilsa lotu", + "costs": "Endurvinnsla: orkukostnaður" }, "pg_sweep": { "optimize": "Bestun: {param}" diff --git a/custom_components/ha_washdata/translations/panel/it.json b/custom_components/ha_washdata/translations/panel/it.json index 988460c4..ab6d70a9 100644 --- a/custom_components/ha_washdata/translations/panel/it.json +++ b/custom_components/ha_washdata/translations/panel/it.json @@ -1,5 +1,7 @@ { "badge": { + "quiet_tail_tip": "Verso la fine questo programma resta in silenzio per circa {mins} min, poi assorbe circa {watts} W per {secs} s prima di terminare. Osservato in {seen} cicli misurati su {measured}: l'elettrodomestico non lo fa a ogni utilizzo.", + "quiet_tail": "silenzio finale ~{mins} min", "artifact_tip": "{n} anomalie rilevate (ad esempio porta aperta a metà ciclo): apri per vederle sul grafico", "auto": "(rilevato automaticamente)", "built_in_tag": "integrato", @@ -15,7 +17,7 @@ "needs_review": "Necessita di revisione", "overrun": "Ha funzionato più a lungo del solito", "poor_fit": "scarsa vestibilità", - "poor_fit_tip": "Cronologia delle partite incoerente: valuta la possibilità di ricostruire questo profilo", + "poor_fit_tip": "Cronologia delle corrispondenze incoerente: valuta la possibilità di ricostruire questo profilo", "restart_gap_tip": "{n} interruzione riavvio HA: il tracciato di potenza ha un buco", "reviewed": "Revisionato", "steady": "→ stabile", @@ -29,11 +31,14 @@ "approved": "Approvato", "awaiting": "In attesa di approvazione", "imported_tip": "Importato dallo store della community. Usato solo per la corrispondenza, non conteggiato nelle statistiche.", + "unmatchable": "corrispondenza impossibile", "not_importable": "n/d qui", "exists": "esiste già", - "backfilled_tip": "Rilevato in uno storico di potenza importato. Influenza solo la corrispondenza dei programmi, non conteggiato nelle statistiche." + "backfilled_tip": "Rilevato in uno storico di potenza importato. Influenza solo la corrispondenza dei programmi, non conteggiato nelle statistiche.", + "armed": "(si applica al prossimo ciclo)" }, "btn": { + "reset_zoom": "Reimposta zoom", "set_brand_model": "Imposta marca e modello", "refresh_catalog": "Aggiorna catalogo", "add_device": "+ Aggiungi dispositivo", @@ -167,6 +172,8 @@ "trim": "Ritaglia", "trim_split": "Ritaglia / Dividi", "undo": "Annulla", + "unlabel_selected": "Rimuovi etichetta dai selezionati ({n})", + "unlabel_selected_tip": "Rimuove l'etichetta di profilo dai cicli selezionati. Restano nella tua cronologia e potranno essere rietichettati in seguito; smettono semplicemente di influenzare questo profilo.", "use": "Usa", "wipe_all": "Cancella tutti i dati", "delete_selected": "Elimina selezionati ({n})", @@ -174,7 +181,6 @@ "pg_apply_best": "Applica migliore", "pg_apply_to_settings": "Salva nelle impostazioni", "pg_apply_to_settings_tip": "Copia i valori che hai modificato qui nelle impostazioni attive di questo dispositivo", - "search": "Cerca", "import": "Importa", "disconnect": "Disconnetti", "connect_github": "Connetti GitHub", @@ -201,7 +207,8 @@ "hist_read_recorder": "Leggi da Home Assistant", "hist_scan": "Cerca cicli", "hist_import_n": "Importa {n} cicli", - "hist_goto_cycles": "Mostrami i cicli" + "hist_goto_cycles": "Mostrami i cicli", + "save_total_cycles": "Salva totale" }, "conflict": { "anti_wrinkle_exit": { @@ -214,6 +221,12 @@ }, "attn_sub": "Correggi i conflitti prima di salvare", "attn_title": "Conflitti nelle impostazioni: {n}", + "price_entity": { + "power_sensor": "Questo è il Sensore di potenza del dispositivo, non un prezzo. Un'entità di prezzo ha la precedenza sul Prezzo energia statico qui sotto, quindi i cicli verrebbero addebitati a watt per kWh - svuota questo campo.", + "energy_sensor": "Questa è l'Entità del contatore di energia del dispositivo, non un prezzo. Un'entità di prezzo ha la precedenza sul Prezzo energia statico qui sotto, quindi i cicli verrebbero addebitati alla lettura del contatore per kWh - svuota questo campo.", + "device_class": "Questo sensore misura {dc}, non un prezzo per kWh. Un'entità di prezzo ha la precedenza sul Prezzo energia statico qui sotto - svuota questo campo oppure puntalo su un sensore di tariffa.", + "unit": "Questo sensore riporta valori in {unit}, che non è un prezzo per kWh. Un'entità di prezzo ha la precedenza sul Prezzo energia statico qui sotto - svuota questo campo oppure puntalo su un sensore di tariffa." + }, "settings_banner": "Conflitti nelle impostazioni: {n}. Controlla le sezioni evidenziate e correggile prima di salvare.", "settings_banner_btn": "Vai al primo", "confidence": { @@ -301,7 +314,7 @@ "pg_settings_source": "Origine delle impostazioni", "phase_catalog": "Catalogo delle fasi", "playground": "Playground", - "process_history": "Storia del processo", + "process_history": "Elabora storico", "settings_history": "Cronologia impostazioni", "status": "Stato", "status_graph": "Grafico dello stato", @@ -312,12 +325,12 @@ "wipe_history": "Cancella la storia", "recommendations": "Consigli ({n})", "pg_detection_params": "Impostazioni di rilevamento", - "pg_timeline": "Cronologia eventi", "pg_outcome": "Esito della simulazione", "pg_across_cycles": "Su tutti i tuoi cicli", "community_store": "Store della community", "online_account": "Store della community e funzionalità online", - "import_power_history": "Importa lo storico di potenza" + "import_power_history": "Importa lo storico di potenza", + "service_status": "Stato della manutenzione" }, "health": { "fair": "Qualità profilo accettabile", @@ -325,6 +338,10 @@ "poor": "⚠ Qualità profilo scarsa" }, "lbl": { + "envelope_position_tip": "Posizione sulla curva registrata del programma corrispondente, {pct}%. È misurata allineando questo ciclo al profilo invece di contare il tempo, quindi resta corretta quando un ciclo dura più o meno del solito. Si aggiorna mentre l'elettrodomestico è in silenzio, perciò può restare indietro durante una fase attiva.", + "envelope_position": "curva {pct}%", + "aria_playground_chart3": "Grafico interattivo della potenza del ciclo: passa il puntatore per leggere tempo/potenza, scorri o pizzica per ingrandire, trascina per spostarti", + "device_unresolved": "Dispositivo non disponibile ({id})", "drag_to_resize": "Trascina per ridimensionare", "actions": "Azioni", "activity": "Attività", @@ -460,7 +477,7 @@ "profile_index": "indice profilo →", "profile_name": "Nome del profilo", "program": "Programma", - "program_tip": "Sovrascrive quale profilo viene abbinato al ciclo corrente. Il rilevamento automatico consente all'integrazione di scegliere automaticamente la corrispondenza migliore. Aggancia un programma specifico per forzare la corrispondenza quando il rilevamento automatico non è corretto o sai cosa è in esecuzione.", + "program_tip": "Sovrascrive quale profilo viene abbinato al ciclo corrente. Il rilevamento automatico consente all'integrazione di scegliere automaticamente la corrispondenza migliore. Aggancia un programma specifico per forzare la corrispondenza quando il rilevamento automatico non è corretto o sai cosa è in esecuzione. Scegline uno prima di avviare l'elettrodomestico e verrà applicato non appena inizia il prossimo ciclo.", "progress": "Progressi", "raw_socket": "Segnale grezzo", "read_only": "Sola lettura", @@ -486,7 +503,7 @@ "settings_basic": "Base", "settings_detail_level": "Livello di dettaglio impostazioni", "settings_overrides": "Override impostazioni", - "show_debug": "Mostra la scheda di debug della partita in tempo reale nella pagina Stato (fiducia, ambiguità, migliori candidati)", + "show_debug": "Mostra la scheda di debug della corrispondenza in tempo reale nella pagina Stato (confidenza, ambiguità, migliori candidati)", "show_expected": "Mostra sovrapposizione curva prevista (profilo corrispondente, arancione)", "show_raw": "Mostra l'interruttore del segnale grezzo della presa nel grafico di potenza in tempo reale", "sparkline": "Andamento recente durata cicli", @@ -495,6 +512,7 @@ "stage4": "Fase 4 – concordanza", "status": "Stato", "tail_trim": "Ritaglio finale (s)", + "tap_to_dismiss": "tocca il grafico per chiudere", "timer_auto_pause": "Pausa automatica", "timer_min": "min", "timer_msg_placeholder": "Messaggio (facoltativo, {device}/{program}/{minutes})", @@ -531,9 +549,7 @@ "pg_time_left_model": "Tempo rimanente (modello)", "pg_time_left_model_tip": "Tempo rimanente stimato dal modello (stimatore di fase + combinazione ML), non un conto alla rovescia fisso", "phase": "Fase", - "pg_mode_simulate": "Simula", "pg_mode_history": "Prova sullo storico", - "pg_mode_sweep": "Scansione", "pg_ev_detected": "Rilevato", "pg_ev_match": "Abbinamento confermato", "pg_ev_match_changed": "Abbinamento cambiato", @@ -572,14 +588,11 @@ "pg_stress_idle_w_desc": "Sostituisce il piano di standby rilevato automaticamente (lasciare vuoto per automatico).", "pg_stress_toggle": "Testa la terminazione a riposo", "pg_stress_toggle_desc": "Simula l'elettrodomestico mantenuto al suo consumo in standby dopo la fine della registrazione – mostra se e quando WashData interrompe il ciclo.", - "pg_sweep_2d": "Mappa di calore 2D", - "pg_sweep_param_y": "2° parametro", "pg_obj_match": "Precisione di abbinamento", "pg_obj_endtiming": "Precisione del momento di fine", "pg_obj_falseend": "Tasso di false conclusioni", "pg_obj_overrun": "Scostamento di durata", "pg_obj_ambiguity": "Tasso di ambiguità", - "pg_best_combo": "migliore combinazione", "pg_mode_optimize": "Ottimizza", "pg_group_matching": "Corrispondenza del programma", "pg_zoom_hint": "fai doppio clic per reimpostare lo zoom", @@ -658,7 +671,12 @@ "task_history_import_apply": "Importazione dei cicli", "evidence_real_cycles": "Cicli eseguiti da questa macchina", "evidence_reference_cycles": "Scaricati dallo store della community", - "evidence_backfill_cycles": "Trovati in uno storico di potenza importato" + "evidence_backfill_cycles": "Trovati in uno storico di potenza importato", + "zoom_hint_touch": "pizzica per ingrandire", + "zoom_hint_touch_zoomed": "pizzica per ingrandire · doppio tocco per reimpostare", + "cycles_since_service": "{since} / {total} cicli", + "total_cycles_run": "cicli eseguiti in totale", + "correct_total_cycles": "Correggi il totale" }, "log": { "all_levels": "Tutti i livelli", @@ -708,7 +726,7 @@ "merge_cycles": "Unisci {n} cicli", "new_group": "Nuovo gruppo di profili", "new_phase": "Nuova Fase", - "process_history_msg": "Riesegui il confronto, aggiorna i suggerimenti, riaddestra i modelli ML (se abilitati) e ricalcola lo stato di tutti i cicli memorizzati. Potrebbe richiedere del tempo.", + "process_history_msg": "Riesegui la corrispondenza, aggiorna i suggerimenti, riaddestra ML (se abilitato), ricalcola il costo dei cicli in base ai prezzi dell'energia registrati e ricalcola lo stato di tutti i cicli memorizzati. Potrebbe richiedere del tempo.", "process_history_ok": "Elabora", "process_history_title": "Elabora storico", "process_recording": "Registrazione del processo", @@ -741,6 +759,7 @@ "history_import": "Importa lo storico di potenza" }, "msg": { + "pg_canvas_empty3": "Scegli un ciclo qui sopra e premi Esegui per simularlo. Poi passa il puntatore per leggere i valori, scorri o pizzica per ingrandire e trascina per spostarti.", "tail_trim_hint": "Numero di secondi da rimuovere dalla fine", "store_sibling_hint": "Non c'è nulla di condiviso per il tuo modello esatto? Un modello molto simile della stessa marca è di solito un buon punto di partenza.", "store_declare_appliance": "Indica a WashData quale elettrodomestico possiedi e questa scheda mostrerà le configurazioni che altre persone hanno condiviso per esso. Puoi anche digitare una marca qui sopra per dare un'occhiata.", @@ -754,7 +773,7 @@ "artifact_spike_detail": "Ha superato la banda di potenza normale per ~{n}s.", "auto_label_intro": "Assegnare profili a cicli senza etichetta la cui confidenza di corrispondenza supera la soglia.", "automations_intro": "WashData genera gli eventi {start} / {end} ed espone entità, quindi le notifiche e le azioni si costruiscono al meglio come normali automatizzazioni di Home Assistant. Le automatizzazioni che utilizzano questo dispositivo appaiono di seguito.", - "cleanup_intro": "Ogni ciclo etichettato è sovrapposto. Spunta i valori anomali ed elimina per ripulire il profilo.", + "cleanup_intro": "Ogni ciclo etichettato è sovrapposto. Spunta i valori anomali, poi rimuovi la loro etichetta (restano nella cronologia, ma non influenzano più questo profilo) oppure eliminali del tutto.", "clear_debug_hint": "Rimuovi i dati di debug archiviati per liberare spazio.", "collecting_data": "Raccolta dati. Cicli ancora necessari prima che l'ottimizzazione possa iniziare: {need} ({current}/{min}).", "compare_overlay_profiles": "Profili sovrapposti (debole)", @@ -853,7 +872,7 @@ "preferences_admin": "Preferenze, pannello e controllo degli accessi", "preferences_adv": "Preferenze", "prefs_personal": "Questi si applicano solo al tuo account Home Assistant.", - "process_history_hint": "Riesegui il confronto su tutti i cicli memorizzati, aggiorna i suggerimenti di ottimizzazione, riaddestra i modelli ML (se abilitati) e ricalcola lo stato dei cicli. Da eseguire dopo una serie di revisioni.", + "process_history_hint": "Riesegui la corrispondenza su tutti i cicli memorizzati, aggiorna i suggerimenti di ottimizzazione, riaddestra i modelli ML (se abilitati), ricalcola il costo dei cicli in base ai prezzi dell'energia registrati e ricalcola lo stato dei cicli. Da eseguire dopo una serie di revisioni.", "profile_deleted": "Profilo eliminato", "profile_poor_health_detail": "I cicli assegnati a questo profilo hanno forme incoerenti o bassa confidenza. Considerare la ricostruzione della busta o la revisione dei cicli etichettati.", "profiles_intro": "Fai clic su un profilo per statistiche, fasi e pulizia. Raggruppa programmi quasi identici (stessa forma/durata, temperatura o centrifuga diversa) in modo che l'abbinamento sia affidabile tra loro.", @@ -936,9 +955,10 @@ "advisory_poor_health": "'{name}' ha un punteggio di corrispondenza basso - i cicli recenti variano molto o corrispondono debolmente. Controlla i suoi cicli o registra di nuovo il profilo affinché il riconoscimento e le stime dei tempi restino accurati.", "advisory_shape_drift": "'{name}' si è discostato notevolmente dalla curva di potenza originale. L'elettrodomestico potrebbe aver cambiato comportamento nel tempo (ad es. calcare, usura). Valuta di registrare di nuovo questo profilo con cicli recenti.", "advisory_shape_drift_corr": "'{name}' si è discostato notevolmente dalla curva di potenza originale (correlazione {corr}). L'elettrodomestico potrebbe aver cambiato comportamento nel tempo (ad es. calcare, usura). Valuta di registrare di nuovo questo profilo con cicli recenti.", + "advisory_unmatchable": "'{name}' non potrà mai trovare corrispondenza: non ha alle spalle alcun ciclo con dati di potenza, quindi WashData non ha nulla con cui confrontare un ciclo in corso. Etichettare un ciclo di questo programma (o registrarne uno) per reinserirlo nella corrispondenza.", + "advisory_unmatchable_no_duration": "'{name}' non potrà mai trovare corrispondenza: ha un ciclo con dati di potenza ma senza una durata utilizzabile, quindi WashData non ha alcuna durata su cui dimensionare una corrispondenza. Registrare di nuovo questo programma, oppure impostarne la durata prevista, per reinserirlo nella corrispondenza.", "pg_no_alerts": "Nessun problema rilevato in questa esecuzione.", "pg_override_active": "Utilizzo delle tue impostazioni modificate vs attuali", - "pg_history_intro": "Riproduci i tuoi cicli recenti con il rilevatore e il sistema di abbinamento reali. Modifica prima un'impostazione di rilevamento in Simula per vedere qui un confronto prima/dopo.", "pg_history_empty": "Premi Esegui per riprodurre i tuoi cicli.", "pg_history_summary": "{detected}/{total} rilevati · {correct} abbinati correttamente", "pg_sweep_no_metric": "Dati insufficienti per valutare questo obiettivo.", @@ -1015,11 +1035,16 @@ "hist_scan_failed": "Scansione non riuscita.", "hist_scan_expired": "Quella scansione non è più disponibile. Esegui di nuovo la scansione.", "hist_import_failed": "Importazione non riuscita.", - "imported_history_readonly": "Rilevato in uno storico di potenza importato. Influenza la corrispondenza dei programmi ma non viene conteggiato nelle tue statistiche e non può essere ritagliato né diviso. Assegna un'etichetta per indicare il programma." + "imported_history_readonly": "Rilevato in uno storico di potenza importato. Influenza la corrispondenza dei programmi ma non viene conteggiato nelle tue statistiche e non può essere ritagliato né diviso. Assegna un'etichetta per indicare il programma.", + "odometer_intro": "Questo totale può solo aumentare. L'eliminazione del record di un ciclo non lo modifica e continua a contare oltre il limite dello storico conservato, così i promemoria di servizio restano corretti.", + "odometer_correct_hint": "Imposta questo valore se WashData ha registrato un ciclo che non è mai avvenuto, oppure se l'elettrodomestico aveva già eseguito dei cicli prima dell'installazione di WashData.", + "no_reminders_set": "Non è ancora impostato alcun promemoria di servizio.", + "toast_program_armed": "Programma preimpostato per il prossimo ciclo: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Lavastoviglie: secondi di silenzio dopo la durata prevista prima che venga rilasciata l'attesa dello scarico di fine ciclo", "smart_termination_duration_ratio": "Frazione della durata prevista del programma corrispondente che un ciclo deve raggiungere prima che la Terminazione intelligente possa concluderlo in anticipo; abbassala per le macchine che dipendono dal carico o dalla temperatura", + "anti_crease_finalize_ratio": "Frazione della durata prevista del programma corrispondente che un ciclo deve raggiungere prima che la coda di rotazione antirughe possa essere finalizzata; abbassala sulle asciugatrici con una coda di rotazione lunga, lascia le lavatrici sul valore predefinito", "anti_wrinkle_enabled": "Assorbire gli impulsi di rotazione dopo la fase principale invece di leggerli come nuovi cicli", "anti_wrinkle_exit_power": "La potenza deve scendere sotto questo valore tra gli impulsi affinché la modalità antirughe resti attiva", "anti_wrinkle_idle_timeout": "Tempo di silenzio consentito tra due impulsi di rotazione prima che la modalità antirughe termini", @@ -1045,7 +1070,8 @@ "dtw_ddtw_scale": "Fase 3: distanza di semi-saturazione DDTW; minore = maggiore sensibilità alla forma (predefinito 30)", "dtw_refine_top_n": "Fase 3: candidati rivalutati da DTW; aumentare a 7-9 se il profilo corretto si classifica 4°-5° (predefinito 5)", "duration_scale": "Fase 4: log-rapporto in cui la concordanza di durata si dimezza; minore = penalità più severa (predefinito 0,175)", - "energy_scale": "Fase 4: log-rapporto in cui la concordanza di energia si dimezza; minore = penalità più severa (predefinito 0,25)" + "energy_scale": "Fase 4: log-rapporto in cui la concordanza di energia si dimezza; minore = penalità più severa (predefinito 0,25)", + "curve_preroll_seconds": "Secondi di letture da tentativi di avvio interrotti riportati all'inizio della curva di un ciclo; 0 = disattivato" }, "phase_desc": { "anti_crease": "Brevi tumble occasionali dopo il completamento per ridurre le grinze del bucato.", @@ -1143,6 +1169,10 @@ "label": "Rilascio al silenzio dell'asciugatura passiva", "doc": "Una volta che il ciclo supera la sua durata prevista, per quanto tempo la lavastoviglie deve restare silenziosa (sotto la Soglia di arresto) prima che WashData smetta di attendere uno scarico finale e termini il ciclo. Aumentalo se la tua macchina ha una lunga fase di asciugatura silenziosa prima di uno scarico finale tardivo che non viene rilevato - una finestra più ampia consente alla durata appresa di seguire la deriva stagionale (acqua in ingresso più fredda = cicli più lunghi) invece di bloccarsi sulla vecchia media. Riduce soltanto l'attesa rispetto al limite interno di 30 minuti per il picco di fine, senza mai prolungarla." }, + "anti_crease_finalize_ratio": { + "label": "Rapporto di finalizzazione antirughe", + "doc": "Quanto avanti nella durata prevista del programma corrispondente deve trovarsi un ciclo prima che WashData possa stabilire che la rotazione a bassa potenza che sta osservando è la coda antirughe e concludere lì il ciclo. La durata prevista è la media del programma, quindi su un'asciugatrice il cui tempo di asciugatura a sonda dipende dal carico la maggior parte dei cicli non raggiunge mai il 98% di quella media e la coda resta in attesa fino al timeout di riserva, spesso da 15 a 40 minuti dopo l'effettivo arresto della macchina. Su un'asciugatrice di questo tipo abbassa il valore (ad esempio 0,75). Lascia le lavatrici sul valore predefinito: una lavatrice trascorre gran parte del ciclo al di sotto della Potenza massima antirughe, quindi questa frazione è l'unica cosa che separa una fase silenziosa a metà lavaggio dalla coda, e abbassarla può dimezzare un lavaggio. Lascia vuoto per il valore predefinito (0,98). Questa impostazione è diversa dal Rapporto di terminazione intelligente, che regola un controllo di fine anticipata separato." + }, "anti_wrinkle_enabled": { "doc": "Riconosci i brevi impulsi di asciugatrice a bassa potenza emessi da un'asciugatrice dopo la fase di riscaldamento principale e mantienili collegati al ciclo finito invece di leggerli come nuovi cicli.", "label": "Abilita rilevamento antirughe" @@ -1164,7 +1194,7 @@ "label": "Potenza massima antirughe" }, "auto_label_confidence": { - "doc": "Se il punteggio della partita alla fine del ciclo è pari o superiore a questo, il programma viene etichettato automaticamente senza alcuna richiesta di conferma. Aumentarlo per richiedere una maggiore certezza prima dell'etichettatura automatica; abbassarlo per automatizzare di più. Funziona insieme al Learning Confidence sottostante.", + "doc": "Se il punteggio di corrispondenza alla fine del ciclo è pari o superiore a questo, il programma viene etichettato automaticamente senza alcuna richiesta di conferma. Aumentarlo per richiedere una maggiore certezza prima dell'etichettatura automatica; abbassarlo per automatizzare di più. Funziona insieme alla Confidenza apprendimento sottostante.", "label": "Confidenza etichettatura automatica" }, "auto_maintenance": { @@ -1175,6 +1205,10 @@ "doc": "I cicli più brevi di questo vengono scartati come cicli fantasma (corse di prova, apertura della porta per aggiungere un calzino).", "label": "Durata minima ciclo" }, + "curve_preroll_seconds": { + "label": "Anticipo della curva", + "doc": "Quanti secondi di letture provenienti da tentativi di avvio interrotti WashData può riportare all'inizio della curva di un ciclo. La curva di un ciclo parte normalmente dal tentativo di avvio che alla fine riesce, quindi su una macchina che fa qualche tentativo prima di partire davvero (selezione del programma, blocco dell'oblò, primo carico d'acqua) i primi minuti di attività reale mancano da ogni curva. Imposta questo valore poco sopra il più lungo avvio a vuoto della tua macchina (300 s bastano e avanzano nella maggior parte dei casi). 0 disattiva la funzione. Tieni presente che attivarlo anticipa l'inizio registrato, quindi i cicli registrati prima e dopo la modifica avranno durate diverse per lo stesso programma; le medie apprese andranno alla deriva finché i cicli più vecchi non usciranno dallo storico." + }, "delay_confirm_seconds": { "doc": "L'alimentazione deve rimanere nella banda di standby per tutto questo tempo prima che l'apparecchio venga considerato in attesa di avvio anziché in funzione.", "label": "Secondi di conferma" @@ -1212,7 +1246,7 @@ "label": "Conteggio ripetizioni fine" }, "energy_price_entity": { - "doc": "Sensor with the current electricity price per kWh (e.g. a dynamic tariff). Ha la precedenza sul prezzo statico riportato di seguito. Ogni ciclo blocca il prezzo in vigore al termine.", + "doc": "Sensore con il prezzo attuale dell'elettricità per kWh (ad esempio una tariffa dinamica). Deve essere un prezzo, non l'entità di potenza o di energia della presa stessa: ha la precedenza sul prezzo statico riportato di seguito, quindi un contatore di kWh inserito qui addebita ogni ciclo alla lettura del contatore invece che alla tua tariffa. Con Costo ponderato nel tempo attivo, ogni ciclo viene addebitato al prezzo in vigore in ogni momento in cui è stato eseguito; altrimenti gli viene fissato il prezzo in vigore al termine.", "label": "Entità prezzo energia" }, "energy_price_static": { @@ -1240,7 +1274,7 @@ "label": "Inverti trigger esterno (attiva su SPENTO)" }, "learning_confidence": { - "doc": "Se il punteggio di corrispondenza è compreso tra questo e la sicurezza dell'etichetta automatica, una notifica di feedback ti chiede di verificare il programma identificato. Al di sotto di questo punteggio la partita è troppo incerta per emergere. Deve essere mantenuto al di sotto del livello di confidenza dell'etichetta automatica.", + "doc": "Se il punteggio di corrispondenza è compreso tra questo e la Confidenza etichettatura automatica, una notifica di feedback ti chiede di verificare il programma identificato. Al di sotto di questo punteggio la corrispondenza è troppo incerta per essere mostrata. Deve essere mantenuto al di sotto della Confidenza etichettatura automatica.", "label": "Confidenza apprendimento" }, "linked_device": { @@ -1308,7 +1342,7 @@ "label": "Genera eventi HA per le notifiche" }, "notify_icon": { - "doc": "Icona mdi opzionale per la notifica (es. mdi:lavatrice). Lascia vuoto per l'impostazione predefinita della piattaforma.", + "doc": "La piccola icona mostrata sulla notifica (es. mdi:washing-machine). Android la disegna nella barra di stato; iOS la mostra al posto dell'icona dell'app e richiede l'app complementare 2026.8 o successiva. Lascia vuoto per l'impostazione predefinita della piattaforma.", "label": "Icona notifica" }, "notify_live_chronometer": { @@ -1408,7 +1442,7 @@ "label": "Soglia di corrispondenza" }, "profile_unmatch_threshold": { - "doc": "Se una partita in diretta a metà ciclo scende al di sotto di questo punteggio, l'identificazione provvisoria viene cancellata. Mantienilo leggermente al di sotto della Soglia di corrispondenza in modo che un breve calo nella somiglianza non riporti la visualizzazione su senza corrispondenza.", + "doc": "Se una corrispondenza in tempo reale a metà ciclo scende al di sotto di questo punteggio, l'identificazione provvisoria viene cancellata. Mantienilo leggermente al di sotto della Soglia di corrispondenza in modo che un breve calo nella somiglianza non riporti la visualizzazione su senza corrispondenza.", "label": "Soglia di non corrispondenza" }, "progress_reset_delay": { @@ -1419,10 +1453,6 @@ "doc": "Secondi in cui una pompa può funzionare continuamente prima che venga contrassegnata come probabilmente bloccata (avvia l'evento pompa bloccata).", "label": "Durata pompa bloccata" }, - "running_dead_zone": { - "doc": "Dopo l'avvio di un ciclo, i cali di potenza all'interno di questa finestra vengono ignorati. Le lavatrici si riempiono con acqua fredda (che scende vicino a 0 W prima del riscaldamento): senza questa protezione la fase di riempimento assomiglia alla fine del ciclo. Ciò NON salta i dati: la traccia completa della potenza viene registrata da T=0. Il motore dei suggerimenti misura l'effettivo modello di avvio della macchina e lo dimensiona automaticamente.", - "label": "Zona morta in esecuzione" - }, "sampling_interval": { "doc": "Tempo previsto tra le letture del sensore: utilizzato per dimensionare la finestra di livellamento e avviare correttamente il rimbalzo. Ogni aggiornamento del sensore viene acquisito indipendentemente da questo valore; calibra solo i calcoli a valle. Il motore dei suggerimenti misura la cadenza effettiva del sensore dai cicli precedenti e la imposta automaticamente.", "label": "Intervallo di campionamento" @@ -1448,7 +1478,7 @@ "label": "Energia avvio" }, "start_threshold_w": { - "doc": "La potenza deve superare questo livello per confermare l'inizio di un ciclo. Un valore troppo basso provoca false partenze dall'alimentazione in standby; vengono persi programmi troppo elevati e con avvio lento (riempimento a freddo). Il motore dei suggerimenti imposta questo valore appena al di sopra della potenza attiva più bassa osservata della macchina.", + "doc": "La potenza deve superare questo livello per confermare l'inizio di un ciclo. Un valore troppo basso provoca false partenze dovute alla potenza in standby; un valore troppo alto fa perdere i programmi ad avvio lento (riempimento a freddo). Il motore dei suggerimenti imposta questo valore appena al di sopra della potenza attiva più bassa osservata sulla macchina.", "label": "Soglia di avvio" }, "stop_threshold_w": { @@ -1550,13 +1580,17 @@ "label": "Intervallo profilo di potenza", "doc": "Dimensione del bucket per l'attributo sensore power_profile per profilo (l'array piatto di watt medi per slot consumato dai pianificatori esterni come EMHASS e tibber_prices). Bucket più piccoli mantengono nitidi i picchi di potenza brevi; bucket più grandi smussano la forma. Predefinito 15 min. Solo in lettura; non influisce sul rilevamento." }, + "notify_live_silent": { + "label": "Aggiornamenti silenziosi in tempo reale", + "doc": "Solo iOS. Aggiorna l'avanzamento in tempo reale in modo discreto: ogni aggiornamento arriva senza suono né vibrazione, con una priorità più bassa che risparmia batteria e che iOS può raggruppare per qualche istante. Il primo aggiornamento di un ciclo (quello che avvia l'Attività in tempo reale) e le notifiche di avvio e di fine avvisano comunque come sempre. Disattiva questa opzione per ricevere un avviso a ogni aggiornamento." + }, "notify_live_sticky": { "label": "Mantieni notifica live al tocco", "doc": "Solo Android. Rende persistente (sticky) la notifica di avanzamento in tempo reale, in modo che toccandola non venga chiusa. Disattivato mantiene il comportamento predefinito in cui il tocco la chiude." }, "notify_live_click_action": { - "label": "Destinazione tocco notifica live", - "doc": "Solo Android. Dove si apre un tocco sulla notifica di avanzamento in tempo reale (ad es. /lovelace/laundry o un URL completo) invece della pagina iniziale dell'app. Lascia vuoto per il valore predefinito." + "label": "Destinazione tocco notifica", + "doc": "Dove si apre un tocco su una notifica di questo elettrodomestico, su Android e iOS. Se lasciato vuoto, apre il pannello WashData su questo elettrodomestico. Inserisci il percorso di una dashboard (ad es. /lovelace/laundry) o un URL completo per aprire altro, oppure \"none\" per nessuna destinazione al tocco." }, "door_opens_at_end": { "label": "Porta si apre automaticamente al termine", @@ -1569,6 +1603,10 @@ "profile_evidence_sources": { "label": "Cicli che formano un programma", "doc": "Quali cicli vengono usati per costruire la curva di potenza di ogni programma e per confrontare con essa un ciclo terminato. Deselezionare un tipo fa sì che non modelli più i tuoi programmi senza eliminare nulla - i cicli restano nel tuo elenco Cicli e possono ancora essere etichettati o rimossi. Utile se non ti fidi dei dati importati. Le statistiche non sono influenzate: conteggiano sempre solo i cicli che questa macchina ha effettivamente eseguito. Deselezionare tutto viene ignorato, perché un programma senza cicli alle spalle non potrebbe mai corrispondere." + }, + "energy_price_dynamic": { + "label": "Costo ponderato nel tempo", + "doc": "Addebita ogni ciclo al prezzo in vigore in ogni momento in cui è stato eseguito, invece dell'unico prezzo valido al termine. Vale solo per un'entità di prezzo (un prezzo statico non può variare). Non serve alcuna configurazione aggiuntiva: il prezzo viene seguito in tempo reale mentre il ciclo è in corso, e Elabora storico, in Diagnostica, ricalcola il costo dei cicli più vecchi a partire dal recorder. Disattivato = il comportamento classico, un unico prezzo fissato alla fine." } }, "setting_group": { @@ -1680,7 +1718,8 @@ "splitting": "Divisione…", "deleting": "Eliminazione…", "imported": "Importato", - "preparing": "Preparazione…" + "preparing": "Preparazione…", + "unlabelling": "Rimozione etichetta…" }, "tab": { "advanced": "Avanzato", @@ -1764,6 +1803,7 @@ "recording_stopped": "La registrazione è stata interrotta", "review_saved": "Revisione salvata", "settings_conflicts": "Correggere i conflitti di impostazioni evidenziati prima di salvare.", + "settings_no_changes": "Nessuna modifica da salvare", "settings_reverted": "Impostazioni ripristinate; ricarica dell'integrazione", "settings_saved": "Impostazioni salvate; ricarica dell'integrazione", "suggestions_applied": "Suggerimenti applicati; ricarica dell'integrazione", @@ -1818,7 +1858,11 @@ "export_selective_done": "Esportazione scaricata", "import_selective_done": "Importati {profiles} profilo/i e {cycles} ciclo/i", "hist_csv_required": "Carica prima un file CSV o incolla il suo contenuto", - "file_read_failed": "Impossibile leggere quel file" + "file_read_failed": "Impossibile leggere quel file", + "unlabel_done": "Etichetta rimossa da {count} cicli", + "unlabel_failed": "Rimozione dell'etichetta non riuscita: {error}", + "odometer_saved": "Totale dei cicli salvato", + "odometer_save_failed": "Impossibile salvare il totale: {error}" }, "suggestion": { "both_agree": "WashData consiglia", @@ -1865,10 +1909,6 @@ "higher": "Conferma solo le corrispondenze ad alta affidabilità; più cicli rimangono senza etichetta", "lower": "Corrisponde a più cicli; alcuni potrebbero essere leggermente errati" }, - "running_dead_zone": { - "higher": "Ignora brevi picchi di potenza durante il riposo – meno sensibile al rumore", - "lower": "Risponde a impulsi più brevi – rileva attività transitorie rapide" - }, "start_threshold_w": { "higher": "Evita avvii falsi causati da brevi picchi di potenza", "lower": "Rileva programmi a bassa potenza; può attivarsi con il rumore" @@ -1893,6 +1933,8 @@ "end_repeat_count": "{false}/{total} cicli puliti ({pct}%) hanno avuto una pausa interna >60s poi ripresa; richiedere {val} conferma/e di fine.", "learning_confidence": "Confidenza p05 di {cycles} cicli etichettati dall'utente ({p05}); al di sotto, viene richiesta una verifica.", "match_interval": "In base alla cadenza di aggiornamento osservata (mediana={median}s) * 10.", + "match_interval_capped": "Limitato affinché {persistence} corrispondenze consecutive rientrino nel {pct}% del programma più breve ({shortest}s); la sola cadenza di aggiornamento (mediana={median}s) avrebbe suggerito un intervallo più lungo.", + "match_interval_floored": "Il solo programma più breve ({shortest}s) limiterebbe questo valore a {cap}s, sotto il minimo di {minimum}s, quindi resta al minimo: {persistence} corrispondenze consecutive richiedono {budget}s, ovvero più del {pct}% di quel programma. La corrispondenza viene eseguita solo all'arrivo di una lettura (mediana={median}s), quindi il minimo non ha alcun costo.", "max_duration_ratio": "In base alle durate dei cicli etichettati (p95={p95}).", "min_active": "In base alla potenza attiva minima ({min}W) osservata nell'ultimo ciclo.", "min_duration_ratio": "Mantenuto il più basso possibile affinché un programma venga riconosciuto presto nel ciclo; i limiti di confidenza e ambiguità impediscono decisioni premature.", @@ -1950,10 +1992,11 @@ "date_tip": "Data e ora di inizio del ciclo.", "duration_tip": "Tempo totale di esecuzione del ciclo, dall'inizio alla fine.", "energy_tip": "Energia totale consumata (kWh). Calcolata integrando la potenza nel tempo.", - "cost_tip": "Costo energetico di questo ciclo, fissato al completamento usando il prezzo allora in vigore (energia x prezzo per kWh). Imposta un prezzo nelle Impostazioni per popolarlo.", + "cost_tip": "Costo energetico di questo ciclo, fissato al completamento. Con Costo ponderato nel tempo attivo, la curva di potenza viene addebitata al prezzo in vigore in ogni momento; altrimenti energia x l'unico prezzo in vigore alla fine. Imposta un prezzo nelle Impostazioni per popolarlo.", "confidence_tip": "Confidenza di corrispondenza del profilo (0-100%). Quanto la curva di potenza del ciclo corrispondeva al programma identificato.", "health_tip": "Salute del ciclo tramite ML (più alto = migliore). Clicca su un ciclo per ispezionarlo e revisionarlo.", - "flags_tip": "Indicatori di revisione, anomalia e fonte per il ciclo. Passa il cursore su un'icona per i dettagli." + "flags_tip": "Indicatori di revisione, anomalia e fonte per il ciclo. Passa il cursore su un'icona per i dettagli.", + "cost_dynamic_tip": "Ponderato nel tempo: addebitato al prezzo in vigore durante il ciclo (prezzo effettivo {price} {cur}/kWh)." }, "stat": { "avg": "media", @@ -1980,9 +2023,9 @@ "finished": "Il ciclo ha raggiunto lo stato finale e si è concluso." }, "store": { + "search_ph2": "Cerca per marca o modello…", "your_model_tip": "Questo è l'elettrodomestico che hai dichiarato nelle Impostazioni", "your_model": "Il tuo", - "search_brand_ph": "Cerca per marca…", "programs_count": "Programmi: {n}", "browse": "Sfoglia", "device": "Dispositivo", @@ -2027,7 +2070,8 @@ "golden": "Rielaborazione: marcatura dei cicli di riferimento", "suggestions": "Rielaborazione: suggerimenti", "ml_training": "Rielaborazione: addestramento ML", - "health": "Rielaborazione: salute dei cicli" + "health": "Rielaborazione: salute dei cicli", + "costs": "Rielaborazione: costi energetici" }, "pg_sweep": { "optimize": "Ottimizza: {param}" diff --git a/custom_components/ha_washdata/translations/panel/ja.json b/custom_components/ha_washdata/translations/panel/ja.json index 7c27d9b6..62f193c5 100644 --- a/custom_components/ha_washdata/translations/panel/ja.json +++ b/custom_components/ha_washdata/translations/panel/ja.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "このプログラムは終盤で約{mins}分間ほとんど電力を使わなくなり、その後、終了前に約{secs}秒間だけ約{watts} W を消費します。測定した{measured}サイクル中{seen}回で確認されました: 毎回そうなるわけではありません。", + "quiet_tail": "静かな終盤 約{mins}分", "artifact_tip": "{n} 個の異常が検出されました (例: サイクルの途中でドアが開いた) – 開いてグラフ上で確認します", "auto": "(自動検出)", "built_in_tag": "内蔵", @@ -77,11 +79,14 @@ "approved": "承認済み", "awaiting": "承認待ち", "imported_tip": "コミュニティストアからインポートしました。マッチングにのみ使用され、統計には含まれません。", + "unmatchable": "マッチング不可", "not_importable": "対象外", "exists": "既存", - "backfilled_tip": "インポートした電力履歴から検出されました。プログラムのマッチングにのみ影響し、統計には含まれません。" + "backfilled_tip": "インポートした電力履歴から検出されました。プログラムのマッチングにのみ影響し、統計には含まれません。", + "armed": "(次のサイクルに適用)" }, "btn": { + "reset_zoom": "ズームをリセット", "set_brand_model": "ブランドとモデルを設定", "refresh_catalog": "カタログを更新", "add_device": "+ デバイスを追加", @@ -215,6 +220,8 @@ "trim": "トリム", "trim_split": "トリム/分割", "undo": "元に戻す", + "unlabel_selected": "選択項目のラベルを解除({n})", + "unlabel_selected_tip": "選択したサイクルからプロファイルのラベルを解除します。サイクルは履歴に残り、後で再びラベル付けできます。このプロファイルを形づくらなくなるだけです。", "use": "使用", "wipe_all": "すべてのデータを消去", "delete_selected": "選択項目を削除({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "最適値を適用", "pg_apply_to_settings": "設定に保存", "pg_apply_to_settings_tip": "ここで編集した値をこのデバイスの現在有効な設定にコピーします", - "search": "検索", "import": "インポート", "disconnect": "切断", "connect_github": "GitHub に接続", @@ -249,7 +255,8 @@ "hist_read_recorder": "Home Assistant から読み込む", "hist_scan": "サイクルを検索", "hist_import_n": "{n} サイクルをインポート", - "hist_goto_cycles": "サイクルを表示" + "hist_goto_cycles": "サイクルを表示", + "save_total_cycles": "累計を保存" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "電源オフしきい値 ({pot} W) を上回る必要があります", "threshold": "有効にするには停止しきい値 ({stop} W) を下回る必要があります" }, + "price_entity": { + "power_sensor": "これはこのデバイスの電力センサーであり、価格ではありません。価格エンティティは下の静的エネルギー価格より優先されるため、ワットの値が kWh あたりの単価として扱われ、サイクルの料金が計算されてしまいます。このフィールドを空にしてください。", + "energy_sensor": "これはこのデバイスのエネルギーメーターであり、価格ではありません。価格エンティティは下の静的エネルギー価格より優先されるため、メーターの読み取り値が kWh あたりの単価として扱われ、サイクルの料金が計算されてしまいます。このフィールドを空にしてください。", + "device_class": "このセンサーが測定しているのは {dc} であり、kWh あたりの価格ではありません。価格エンティティは下の静的エネルギー価格より優先されます。このフィールドを空にするか、電力料金単価のセンサーを指定してください。", + "unit": "このセンサーの読み取り単位は {unit} で、kWh あたりの価格ではありません。価格エンティティは下の静的エネルギー価格より優先されます。このフィールドを空にするか、電力料金単価のセンサーを指定してください。" + }, "pump_stuck": { "duration": "ウォッチドッグがサイクルを強制終了する前にスタックアラームが発動するよう、更新なしタイムアウト ({to} 秒) より小さい必要があります", "timeout": "サイクルが強制停止される前にスタックアラームが発動するよう、ポンプ停止継続時間 ({ps} 秒) を超える必要があります" @@ -360,12 +373,12 @@ "wipe_history": "履歴を消去", "recommendations": "推奨事項({n})", "pg_detection_params": "検出設定", - "pg_timeline": "イベントタイムライン", "pg_outcome": "シミュレーション結果", "pg_across_cycles": "全サイクルにわたって", "community_store": "コミュニティストア", "online_account": "コミュニティストアとオンライン機能", - "import_power_history": "電力履歴のインポート" + "import_power_history": "電力履歴のインポート", + "service_status": "メンテナンス状況" }, "health": { "fair": "プロファイル品質: 普通", @@ -373,6 +386,10 @@ "poor": "⚠ プロファイル品質: 低" }, "lbl": { + "envelope_position_tip": "マッチしたプログラム自身の記録された曲線上の位置、{pct}%。時間を数えるのではなく、このサイクルをプロファイルに重ね合わせて測定するため、いつもより長い運転でも短い運転でも正確なままです。機器が静かなときに更新されるため、稼働中のフェーズでは遅れることがあります。", + "envelope_position": "曲線 {pct}%", + "aria_playground_chart3": "インタラクティブなサイクル電力グラフ: ホバーで時刻/電力を表示、スクロールまたはピンチでズーム、ドラッグで移動", + "device_unresolved": "利用不可のデバイス ({id})", "drag_to_resize": "ドラッグしてサイズ変更", "actions": "アクション", "activity": "活動", @@ -508,7 +525,7 @@ "profile_index": "プロファイル番号 →", "profile_name": "プロファイル名", "program": "プログラム", - "program_tip": "現在のサイクルに一致するプロファイルを上書きします。自動検出では、統合が最適な一致を自動的に選択します。自動検出が誤っている場合や実行中のプログラムが分かっている場合は、特定のプログラムを固定して強制的に一致させます。", + "program_tip": "現在のサイクルに一致するプロファイルを上書きします。自動検出では、統合が最適な一致を自動的に選択します。自動検出が誤っている場合や実行中のプログラムが分かっている場合は、特定のプログラムを固定して強制的に一致させます。家電を運転する前に選んでおくと、次のサイクルが始まった時点で適用されます。", "progress": "進捗", "raw_socket": "生のソケット", "read_only": "読み取り専用", @@ -543,6 +560,7 @@ "stage4": "ステージ 4 – 一致度", "status": "状態", "tail_trim": "末尾のトリム (秒)", + "tap_to_dismiss": "グラフをタップして閉じる", "timer_auto_pause": "自動一時停止", "timer_min": "分", "timer_msg_placeholder": "メッセージ (オプション、{device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "残り時間(モデル)", "pg_time_left_model_tip": "モデルが推定する残り時間(フェーズ推定+MLブレンド)。固定のカウントダウンではありません。", "phase": "フェーズ", - "pg_mode_simulate": "シミュレート", "pg_mode_history": "履歴テスト", - "pg_mode_sweep": "スイープ", "pg_ev_detected": "検出", "pg_ev_match": "マッチ確定", "pg_ev_match_changed": "マッチ変更", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "自動検出された待機電力下限を上書きします(自動の場合は空欄)。", "pg_stress_toggle": "待機終了をテスト", "pg_stress_toggle_desc": "記録終了後に待機電力レベルで稼働し続ける機器をシミュレートします - WashData がサイクルを停止するかどうか、およびいつ停止するかを確認します。", - "pg_sweep_2d": "2Dヒートマップ", - "pg_sweep_param_y": "2番目のパラメーター", "pg_obj_match": "マッチ精度", "pg_obj_endtiming": "終了タイミング精度", "pg_obj_falseend": "誤終了率", "pg_obj_overrun": "所要時間のずれ", "pg_obj_ambiguity": "あいまい率", - "pg_best_combo": "最適な組み合わせ", "pg_mode_optimize": "最適化", "pg_group_matching": "プログラムマッチング", "pg_zoom_hint": "ダブルクリックでズームをリセット", @@ -706,7 +719,12 @@ "task_history_import_apply": "サイクルのインポート", "evidence_real_cycles": "この機器が実行したサイクル", "evidence_reference_cycles": "コミュニティストアからダウンロード", - "evidence_backfill_cycles": "インポートした電力履歴から検出" + "evidence_backfill_cycles": "インポートした電力履歴から検出", + "zoom_hint_touch": "ピンチでズーム", + "zoom_hint_touch_zoomed": "ピンチでズーム · ダブルタップでリセット", + "cycles_since_service": "{since} / {total} サイクル", + "total_cycles_run": "累計サイクル数", + "correct_total_cycles": "累計を修正" }, "log": { "all_levels": "すべてのレベル", @@ -756,7 +774,7 @@ "merge_cycles": "{n} サイクルを結合", "new_group": "新しいプロファイルグループ", "new_phase": "新しいフェーズ", - "process_history_msg": "マッチングを再実行し、提案を更新し、ML を再訓練(有効な場合)し、保存されているすべてのサイクルのサイクル状態を再計算します。これには時間がかかる場合があります。", + "process_history_msg": "保存されているすべてのサイクルについて、マッチングを再実行し、提案を更新し、ML を再訓練(有効な場合)し、記録されたエネルギー価格で料金を計算し直し、サイクルの健全性を再計算します。これには時間がかかる場合があります。", "process_history_ok": "処理", "process_history_title": "履歴の処理", "process_recording": "プロセスの記録", @@ -789,6 +807,7 @@ "history_import": "電力履歴のインポート" }, "msg": { + "pg_canvas_empty3": "上でサイクルを選び、「実行」を押してシミュレーションします。その後、ホバーで値を表示、スクロールまたはピンチでズーム、ドラッグで移動できます。", "tail_trim_hint": "末尾から削除する秒数", "store_sibling_hint": "お使いの機種そのものが共有されていない場合は、同じブランドの近い機種から始めるとうまくいくことが多いです。", "store_declare_appliance": "お使いの家電を WashData に指定すると、このタブに他のユーザーが共有したプログラムや録音が表示されます。上でブランド名を入力して見て回ることもできます。", @@ -802,7 +821,7 @@ "artifact_spike_detail": "通常の電力帯の上側に約{n}秒間現れました。", "auto_label_intro": "一致信頼度がしきい値をクリアするラベルのないサイクルにプロファイルを割り当てます。", "automations_intro": "WashData は {start} / {end} イベントを発火しエンティティを公開するため、通知やアクションは通常の Home Assistant オートメーションとして構築するのが最適です。このデバイスを使用するオートメーションは以下に表示されます。", - "cleanup_intro": "すべてのラベル付きサイクルがオーバーレイされます。外れ値にチェックを入れて削除し、プロファイルをクリーンアップします。", + "cleanup_intro": "ラベル付きのサイクルをすべて重ねて表示します。外れ値にチェックを入れて、ラベルを解除する(履歴には残りますが、このプロファイルを形づくらなくなります)か、完全に削除します。", "clear_debug_hint": "保存されているデバッグ データを削除して領域を解放します。", "collecting_data": "データを収集中です。微調整を開始するために必要な残りサイクル数: {need}({current}/{min})。", "compare_overlay_profiles": "オーバーレイプロファイル (淡い)", @@ -901,7 +920,7 @@ "preferences_admin": "設定、パネル、アクセス制御", "preferences_adv": "設定", "prefs_personal": "これらはホーム アシスタント アカウントにのみ適用されます。", - "process_history_hint": "保存されているすべてのサイクルに対してマッチングを再実行し、チューニング提案を更新し、MLモデルを再訓練し(有効な場合)、サイクルの健全性を再計算します。一括レビュー後に実行してください。", + "process_history_hint": "保存されているすべてのサイクルに対してマッチングを再実行し、チューニング提案を更新し、MLモデルを再訓練し(有効な場合)、記録されたエネルギー価格でサイクルの料金を計算し直し、サイクルの健全性を再計算します。一括レビュー後に実行してください。", "profile_deleted": "プロフィールを削除しました", "profile_poor_health_detail": "このプロファイルに割り当てられたサイクルには、形状が一貫していないか、信頼性が低いです。エンベロープを再構築するか、ラベル付きサイクルを確認することを検討してください。", "profiles_intro": "プロファイルをクリックすると、統計、フェーズ、クリーンアップが表示されます。ほぼ同一のプログラム (同じ形状/持続時間、異なる温度またはスピン) をグループ化して、それらの間でマッチングを確実に選択します。", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' の適合スコアが低くなっています。最近のサイクルはばらつきが大きい、または照合が弱いようです。照合と時間推定の精度を保つため、サイクルを確認するかプロファイルを再記録してください。", "advisory_shape_drift": "'{name}' が元の電力波形から大きく変化しています。機器の動作が時間とともに変わった可能性があります(例: 石灰の付着、摩耗)。最近のサイクルでこのプロファイルを再記録することを検討してください。", "advisory_shape_drift_corr": "'{name}' が元の電力波形から大きく変化しています(相関 {corr})。機器の動作が時間とともに変わった可能性があります(例: 石灰の付着、摩耗)。最近のサイクルでこのプロファイルを再記録することを検討してください。", + "advisory_unmatchable": "'{name}' はマッチングに使用できません。電力データを含むサイクルが1つもないため、WashData が実行中のサイクルと比較する対象がありません。このプログラムのサイクルにラベルを付ける(またはサイクルを記録する)と、再びマッチングの対象になります。", + "advisory_unmatchable_no_duration": "'{name}' はマッチングに使用できません。電力データを含むサイクルはありますが、有効な所要時間がないため、WashData にはマッチングの基準となる長さがありません。このプログラムを記録し直すか、予想所要時間を設定すると、再びマッチングの対象になります。", "pg_no_alerts": "この実行で問題は検出されませんでした。", "pg_override_active": "編集した設定を現在の設定と比較して使用中", - "pg_history_intro": "直近のサイクルを実際の検出器とマッチャーで再生します。ビフォー/アフターの比較を見るには、まず「シミュレート」で検出設定を編集してください。", "pg_history_empty": "「実行」を押してサイクルを再生します。", "pg_history_summary": "{detected}/{total} 検出 · {correct} 正しくマッチ", "pg_sweep_no_metric": "この目的を評価するにはデータが不足しています。", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "スキャンに失敗しました。", "hist_scan_expired": "そのスキャン結果はすでに利用できません。もう一度スキャンしてください。", "hist_import_failed": "インポートに失敗しました。", - "imported_history_readonly": "インポートした電力履歴から検出されました。プログラムのマッチングには反映されますが、統計には含まれず、トリミングや分割はできません。ラベル付けしてプログラム名を設定してください。" + "imported_history_readonly": "インポートした電力履歴から検出されました。プログラムのマッチングには反映されますが、統計には含まれず、トリミングや分割はできません。ラベル付けしてプログラム名を設定してください。", + "odometer_intro": "この累計は増える一方です。サイクルの記録を削除しても変わらず、保存される履歴の上限を超えてもカウントを続けるため、サービスリマインダーは正確なままです。", + "odometer_correct_hint": "WashData が実際には行われていない稼働を記録した場合や、WashData を導入する前に家電がすでにサイクルを実行していた場合は、この値を設定してください。", + "no_reminders_set": "サービスリマインダーはまだ設定されていません。", + "toast_program_armed": "次のサイクル用にプログラムを設定しました: {program}" }, "pg_desc": { "completion_min_seconds": "実サイクルとみなす最短の稼働時間", @@ -1093,7 +1117,9 @@ "duration_scale": "ステージ 4: 運転時間の一致スコアが半分になる対数比率; 小さいほど厳格なペナルティ(デフォルト 0.175)", "energy_scale": "ステージ 4: エネルギーの一致スコアが半分になる対数比率; 小さいほど厳格なペナルティ(デフォルト 0.25)", "dishwasher_end_spike_quiet_release": "食器洗い機: 予想所要時間を過ぎた後、サイクル終了時の排水待機を解除するまでの静音秒数", - "smart_termination_duration_ratio": "一致したプログラムの想定所要時間のうち、スマート終了が早期に終了できるようになるまでにサイクルが到達すべき割合。負荷や温度に左右されるマシンでは下げる" + "smart_termination_duration_ratio": "一致したプログラムの想定所要時間のうち、スマート終了が早期に終了できるようになるまでにサイクルが到達すべき割合。負荷や温度に左右されるマシンでは下げる", + "anti_crease_finalize_ratio": "しわ防止のタンブル末尾を確定してよくなるまでに、一致したプログラムの想定所要時間に対してサイクルが到達すべき割合。タンブル末尾が長い乾燥機では下げ、洗濯機はデフォルトのままにする", + "curve_preroll_seconds": "中断された開始試行の測定値をサイクルの曲線の先頭に取り込む秒数。0 = 無効" }, "phase_desc": { "anti_crease": "メインサイクル後にドラムが断続的に回転し、衣類のシワを防止します。", @@ -1256,7 +1282,7 @@ "label": "終了繰り返し回数" }, "energy_price_entity": { - "doc": "現在の kWh あたりの電力料金 (動的料金など) を示すセンサー。以下の静的価格よりも優先されます。各サイクルは、終了時に有効な価格を凍結します。", + "doc": "現在の kWh あたりの電力価格を示すセンサー(動的料金など)。価格を示すものである必要があり、プラグ自身の電力エンティティやエネルギーエンティティは指定できません。以下の静的価格よりも優先されるため、ここに kWh の積算メーターを指定すると、各サイクルが料金単価ではなくメーターの読み取り値で計算されてしまいます。「時間加重コスト」がオンの場合、各サイクルは動作していた各時点で有効だった価格で計算されます。オフの場合は、終了時に有効だった価格で確定されます。", "label": "エネルギー価格エンティティ" }, "energy_price_static": { @@ -1284,7 +1310,7 @@ "label": "外部トリガーを反転(OFFでトリガー)" }, "learning_confidence": { - "doc": "一致スコアがこの値と自動ラベルの信頼度の間にある場合、識別されたプログラムを確認するように求めるフィードバック通知が表示されます。このスコアを下回ると、試合は不確実すぎて表面化できません。自動ラベルの信頼度以下に保つ必要があります。", + "doc": "一致スコアがこの値と自動ラベル信頼度の間にある場合、識別されたプログラムの確認を求めるフィードバック通知が届きます。このスコアを下回ると、一致が不確実すぎるため表示されません。自動ラベル信頼度より低く保つ必要があります。", "label": "学習信頼度" }, "linked_device": { @@ -1352,7 +1378,7 @@ "label": "通知用HAイベントを発火" }, "notify_icon": { - "doc": "通知のオプションの mdi アイコン (例: mdi:washing-machine)。プラットフォームのデフォルトの場合は空白のままにします。", + "doc": "通知に表示される小さなアイコン(例: mdi:washing-machine)。Android はステータスバーに表示し、iOS はアプリのアイコンの代わりに表示します(コンパニオンアプリ 2026.8 以降が必要)。プラットフォームのデフォルトを使う場合は空欄のままにします。", "label": "通知アイコン" }, "notify_live_chronometer": { @@ -1463,10 +1489,6 @@ "doc": "ポンプがスタックしている可能性があるとフラグが立てられる (スタックポンプ イベントが発生する) までに、ポンプが継続的に実行できる秒数。", "label": "ポンプ停止継続時間" }, - "running_dead_zone": { - "doc": "サイクルの開始後、このウィンドウ内の電力低下は無視されます。洗濯機には冷水が充填されます (加熱前に 0 W 近くに低下します)。この保護がなければ、充填段階がサイクル終了のように見えます。これはデータをスキップしません。フルパワートレースは T=0 から記録されます。提案エンジンはマシンの実際の起動パターンを測定し、自動的にサイズを決定します。", - "label": "実行デッドゾーン" - }, "sampling_interval": { "doc": "センサー読み取り間の予想時間 - スムージング ウィンドウのサイズを調整し、デバウンスを正しく開始するために使用されます。この値に関係なく、すべてのセンサーの更新がキャプチャされます。下流の計算のみを調整します。提案エンジンは、過去のサイクルからセンサーの実際のケイデンスを測定し、これを自動的に設定します。", "label": "サンプリング間隔" @@ -1594,13 +1616,17 @@ "label": "電力プロファイル間隔", "doc": "EMHASS や tibber_prices などの外部プランナーが使用する per-profile の power_profile センサー属性(スロットごとの平均ワット数の配列)のバケットサイズ。小さいバケットは短い電力スパイクを鮮明に保ち、大きいバケットは形状を平滑化します。デフォルト 15 分。読み取り時のみ使用され、検出には影響しません。" }, + "notify_live_silent": { + "label": "サイレントなライブ更新", + "doc": "iOS のみ。ライブ進捗を静かに更新します。各更新はサウンドや振動なしで届き、バッテリーを節約する低い優先度で送信されるため、iOS がまとめて配信することがあります。サイクルの最初の更新(ライブアクティビティを開始する更新)と開始/完了の通知は、これまでどおり通知されます。すべての更新で通知を受け取りたい場合は、これをオフにしてください。" + }, "notify_live_sticky": { "label": "タップ後もライブ通知を維持", "doc": "Android のみ。実行中のスレッドをタップしても通知が消えないよう、ライブ進捗通知を固定(スティッキー)にします。オフにすると、タップで通知が消えるデフォルトの動作になります。" }, "notify_live_click_action": { - "label": "ライブ通知のタップ先", - "doc": "Android のみ。ライブ進捗通知をタップしたときに開く場所(例:/lovelace/laundry、またはフル URL)をアプリのデフォルトページの代わりに指定します。空白のままにするとデフォルトになります。" + "label": "通知のタップ先", + "doc": "この家電の通知をタップしたときに開く場所を、Android と iOS の両方で指定します。空白のままにすると、この家電を表示した WashData パネルが開きます。別の場所を開くにはダッシュボードのパス(例: /lovelace/laundry)またはフル URL を入力し、タップ先を設けない場合は「none」と入力します。" }, "door_opens_at_end": { "label": "終了時に自動でドアが開く", @@ -1617,6 +1643,18 @@ "profile_evidence_sources": { "label": "プログラムを形づくるサイクル", "doc": "各プログラムの電力カーブを作成し、完了したサイクルをそれと照合するために使用するサイクルの種類です。種類のチェックを外すと、その種類はプログラムに反映されなくなりますが、何も削除されません。サイクルはサイクル一覧に残り、ラベル付けや削除も引き続きできます。インポートしたデータを信頼できない場合に便利です。統計には影響しません。統計は常に、この機器が実際に実行したサイクルのみを数えます。すべてのチェックを外した場合は無視されます。裏付けとなるサイクルがないプログラムは、決してマッチングできないためです。" + }, + "energy_price_dynamic": { + "label": "時間加重コスト", + "doc": "終了時点の価格ひとつで計算するのではなく、サイクルが動作していた各時点で有効だった価格で料金を計算します。価格エンティティを設定している場合のみ有効です(静的価格は変動しないため)。追加の設定は不要です。サイクルの実行中は価格をリアルタイムで追跡し、診断の「履歴の処理」を実行すると過去のサイクルも recorder の記録から再計算されます。オフ = 従来どおり、終了時の価格ひとつで確定します。" + }, + "anti_crease_finalize_ratio": { + "label": "しわ防止確定比率", + "doc": "一致したプログラムの想定所要時間のうち、サイクルがどこまで進んだら、WashData が「いま見えている低電力のタンブルはしわ防止の末尾である」と判断してそこでサイクルを終了してよいかを表す割合です。想定所要時間はそのプログラムの平均値なので、センサー乾燥で運転時間が洗濯物の量に左右される乾燥機では、ほとんどの運転がその 98% に届かず、末尾はフォールバックのタイムアウトまで残り続けます。実際に機械が停止してから 15 ~ 40 分後になることもよくあります。そのような乾燥機ではこの値を下げてください(例: 0.75)。洗濯機はデフォルトのままにしてください。洗濯機はサイクルの大半を「しわ防止最大電力」より下で過ごすため、静かな洗濯途中のフェーズと末尾を隔てているのはこの割合だけであり、下げると洗濯が途中で半分に切られてしまうことがあります。空欄にするとデフォルト(0.98)になります。これは、別の早期終了判定を制御する「スマート終了比率」とは異なる設定です。" + }, + "curve_preroll_seconds": { + "label": "曲線のプリロール", + "doc": "中断された開始試行の測定値を、サイクルの曲線の先頭に何秒ぶん取り込めるかを指定します。サイクルの曲線は通常、最終的に成功した開始試行から始まります。そのため、プログラム選択、ドアロック、最初の給水などで数回試してから安定するマシンでは、実際の動作の最初の数分がどの曲線からも欠落します。お使いのマシンで最も長い立ち上がりより少し大きい値を設定してください(多くの場合 300 秒あれば十分です)。0 にすると無効になります。有効にすると記録される開始時刻が前倒しになるため、変更の前後に記録されたサイクルは同じプログラムでも所要時間が異なります。古いサイクルが入れ替わるまで、学習された平均値はずれ続けます。" } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "分割中…", "deleting": "削除中…", "imported": "インポート済み", - "preparing": "準備中…" + "preparing": "準備中…", + "unlabelling": "ラベル解除中…" }, "tab": { "advanced": "高度な", @@ -1808,6 +1847,7 @@ "recording_stopped": "録音停止", "review_saved": "レビューが保存されました", "settings_conflicts": "保存する前に強調表示されている設定の競合を解消してください。", + "settings_no_changes": "保存する変更はありません", "settings_reverted": "設定が復元されました。統合のリロード", "settings_saved": "設定が保存されました。統合のリロード", "suggestions_applied": "提案が適用されました。統合のリロード", @@ -1866,7 +1906,11 @@ "export_selective_done": "エクスポートをダウンロードしました", "import_selective_done": "{profiles} 件のプロファイルと {cycles} 件のサイクルをインポートしました", "hist_csv_required": "先に CSV ファイルを読み込むか、内容を貼り付けてください", - "file_read_failed": "そのファイルを読み込めませんでした" + "file_read_failed": "そのファイルを読み込めませんでした", + "unlabel_done": "{count} 件のサイクルのラベルを解除しました", + "unlabel_failed": "ラベルの解除に失敗しました: {error}", + "odometer_saved": "累計サイクル数を保存しました", + "odometer_save_failed": "累計を保存できませんでした: {error}" }, "suggestion": { "both_agree": "WashDataの推奨", @@ -1913,10 +1957,6 @@ "higher": "高い確信度のマッチングのみをコミットします。より多くのサイクルがラベルなしのままになります", "lower": "より多くのサイクルをマッチングします。一部が若干間違っている場合があります" }, - "running_dead_zone": { - "higher": "アイドル中の短い電力スパイクを無視します。ノイズへの感度が低くなります", - "lower": "より短い電力バーストに反応します。短時間の一時的なアクティビティを検出します" - }, "start_threshold_w": { "higher": "短い電力スパイクによる誤起動を防ぎます", "lower": "低電力プログラムを検出します。ノイズでトリガーされる場合があります" @@ -1962,7 +2002,9 @@ "thr_batch": "{cycles} サイクルにわたる p05 の最低稼働電力({p05}W)のすぐ上に設定。これにより開始をできるだけ早く捉え、停止しきい値は機器の最低稼働電力を下回ったままになります。", "tol_per_profile": "{profiles} 件のプロファイル({cycles} サイクル)にわたるプロファイルごとの時間分散の p75。ばらつきの小さいプロファイルは不利になりません。", "tol_pooled": "{cycles} 件の直近のラベル付きサイクルの統合時間分散に基づく(p95 偏差={dev})。", - "watchdog": "安全な範囲でできるだけ低く設定(p95 の更新間隔 {p95}s をわずかに上回り、かつサンプリング間隔 {median}s の 2 倍以上、最小 30s)。誤った停止なしに停滞を素早く検出します。" + "watchdog": "安全な範囲でできるだけ低く設定(p95 の更新間隔 {p95}s をわずかに上回り、かつサンプリング間隔 {median}s の 2 倍以上、最小 30s)。誤った停止なしに停滞を素早く検出します。", + "match_interval_capped": "{persistence} 回連続のマッチが最短プログラム({shortest}s)の {pct}% 以内に収まるよう上限を設けました。更新間隔(中央値={median}s)だけならより長い間隔が提案されていました。", + "match_interval_floored": "最短プログラム({shortest} 秒)だけで計算すると上限は {cap} 秒となり、最小値の {minimum} 秒を下回るため、最小値で止めています。{persistence} 回連続のマッチングには {budget} 秒かかり、これはそのプログラムの {pct}% を超えます。マッチングは測定値が届いたときにのみ実行されるため(中央値={median} 秒)、最小値にしてもコストはかかりません。" }, "exclusions": { "summary": "{total} 件の誤検出サイクルを除外しました: {parts}。", @@ -1998,10 +2040,11 @@ "date_tip": "サイクルが開始された日時。", "duration_tip": "開始から終了までのサイクル全体の実行時間。", "energy_tip": "消費された総エネルギー(kWh)。電力を時間で積分して計算されます。", - "cost_tip": "このサイクルのエネルギー料金。完了時にその時点で有効な価格(エネルギー x kWh あたりの価格)で確定されます。設定で価格を指定すると入力されます。", + "cost_tip": "このサイクルのエネルギー料金。完了時に確定されます。「時間加重コスト」がオンの場合は、電力トレースの各時点で有効だった価格で計算されます。オフの場合はエネルギー x 終了時に有効だった価格ひとつです。設定で価格を指定すると入力されます。", "confidence_tip": "プロファイルのマッチ信頼度(0~100%)。サイクルの電力曲線が識別されたプログラムとどれだけ一致したか。", "health_tip": "ML サイクルの健康度(高いほど良好)。サイクルをクリックして検査・レビューします。", - "flags_tip": "サイクルのレビュー、異常、ソースフラグ。アイコンにカーソルを合わせると詳細が表示されます。" + "flags_tip": "サイクルのレビュー、異常、ソースフラグ。アイコンにカーソルを合わせると詳細が表示されます。", + "cost_dynamic_tip": "時間加重: サイクル中に有効だった価格で計算されています(実効 {price} {cur}/kWh)。" }, "stat": { "avg": "平均", @@ -2028,9 +2071,9 @@ "finished": "サイクルが終了状態に達して終了しました。" }, "store": { + "search_ph2": "メーカーまたは型番で検索…", "your_model_tip": "設定で指定した家電です", "your_model": "自分の機種", - "search_brand_ph": "ブランドで検索…", "programs_count": "プログラム: {n}", "browse": "閲覧", "device": "デバイス", @@ -2076,7 +2119,8 @@ "golden": "再処理: 基準サイクルの補完", "suggestions": "再処理: 提案", "ml_training": "再処理: MLトレーニング", - "health": "再処理: サイクルの健康" + "health": "再処理: サイクルの健康", + "costs": "再処理: エネルギー料金" }, "pg_sweep": { "optimize": "最適化: {param}" diff --git a/custom_components/ha_washdata/translations/panel/ko.json b/custom_components/ha_washdata/translations/panel/ko.json index 480884af..cef8352c 100644 --- a/custom_components/ha_washdata/translations/panel/ko.json +++ b/custom_components/ha_washdata/translations/panel/ko.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "이 프로그램은 끝날 무렵 약 {mins}분 동안 전력을 거의 쓰지 않다가, 종료 전에 약 {secs}초 동안 약 {watts} W를 사용합니다. 측정된 {measured}회 사이클 중 {seen}회에서 확인되었습니다: 기기가 매번 이렇게 동작하지는 않습니다.", + "quiet_tail": "조용한 마무리 ~{mins}분", "artifact_tip": "{n} 이상 현상이 감지되었습니다(예: 사이클 중간에 문이 열림) - 열어서 그래프에서 확인", "auto": "(자동 감지)", "built_in_tag": "내장", @@ -77,11 +79,14 @@ "approved": "승인됨", "awaiting": "승인 대기 중", "imported_tip": "커뮤니티 스토어에서 가져왔습니다. 매칭에만 사용되며 통계에는 포함되지 않습니다.", + "unmatchable": "매칭 불가", "not_importable": "해당 없음", "exists": "존재함", - "backfilled_tip": "가져온 전력 이력에서 감지되었습니다. 프로그램 매칭에만 영향을 주며 통계에는 포함되지 않습니다." + "backfilled_tip": "가져온 전력 이력에서 감지되었습니다. 프로그램 매칭에만 영향을 주며 통계에는 포함되지 않습니다.", + "armed": "(다음 사이클에 적용)" }, "btn": { + "reset_zoom": "확대/축소 초기화", "set_brand_model": "브랜드 및 모델 설정", "refresh_catalog": "카탈로그 새로 고침", "add_device": "+ 장치 추가", @@ -215,6 +220,8 @@ "trim": "자르기", "trim_split": "자르기/분할", "undo": "실행 취소", + "unlabel_selected": "선택 항목 라벨 해제 ({n})", + "unlabel_selected_tip": "선택한 사이클에서 프로필 라벨을 해제합니다. 사이클은 기록에 그대로 남아 있고 나중에 다시 라벨을 지정할 수 있으며, 이 프로필을 형성하지 않게 될 뿐입니다.", "use": "사용", "wipe_all": "모든 데이터 지우기", "delete_selected": "선택 항목 삭제 ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "최적 값 적용", "pg_apply_to_settings": "설정에 저장", "pg_apply_to_settings_tip": "여기서 편집한 값을 이 기기에 현재 적용 중인 설정으로 복사합니다", - "search": "검색", "import": "가져오기", "disconnect": "연결 해제", "connect_github": "GitHub 연결", @@ -249,7 +255,8 @@ "hist_read_recorder": "Home Assistant에서 읽기", "hist_scan": "사이클 검색", "hist_import_n": "{n}개 사이클 가져오기", - "hist_goto_cycles": "사이클 보기" + "hist_goto_cycles": "사이클 보기", + "save_total_cycles": "누적 값 저장" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "전원 꺼짐 임계값 ({pot} W) 이상이어야 합니다", "threshold": "적용되려면 정지 임계값 ({stop} W) 미만이어야 합니다" }, + "price_entity": { + "power_sensor": "이것은 이 기기의 전력 센서이며 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선하므로, 와트 값이 kWh당 단가로 사용되어 사이클 비용이 계산됩니다. 이 필드를 비워 주세요.", + "energy_sensor": "이것은 이 기기의 에너지 계량기이며 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선하므로, 계량기 값이 kWh당 단가로 사용되어 사이클 비용이 계산됩니다. 이 필드를 비워 주세요.", + "device_class": "이 센서가 측정하는 값은 {dc}이며, kWh당 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선합니다. 이 필드를 비우거나 전기 요금 단가 센서를 지정하세요.", + "unit": "이 센서의 읽기 단위는 {unit}이며, 이는 kWh당 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선합니다. 이 필드를 비우거나 전기 요금 단가 센서를 지정하세요." + }, "pump_stuck": { "duration": "워치독이 사이클을 강제 종료하기 전에 정지 알람이 작동하도록 업데이트 없음 타임아웃 ({to} 초) 미만이어야 합니다", "timeout": "사이클이 강제 정지되기 전에 정지 알람이 작동하도록 펌프 정지 지속 시간 ({ps} 초)을 초과해야 합니다" @@ -360,12 +373,12 @@ "wipe_history": "기록 지우기", "recommendations": "권장 사항 ({n})", "pg_detection_params": "감지 설정", - "pg_timeline": "이벤트 타임라인", "pg_outcome": "시뮬레이션 결과", "pg_across_cycles": "전체 사이클에 걸쳐", "community_store": "커뮤니티 스토어", "online_account": "커뮤니티 스토어 및 온라인 기능", - "import_power_history": "전력 이력 가져오기" + "import_power_history": "전력 이력 가져오기", + "service_status": "유지보수 상태" }, "health": { "fair": "프로파일 품질: 보통", @@ -373,6 +386,10 @@ "poor": "⚠ 프로파일 품질: 불량" }, "lbl": { + "envelope_position_tip": "매칭된 프로그램이 기록한 곡선상의 위치, {pct}%입니다. 시간을 세는 대신 이 사이클을 프로필에 정렬해 측정하므로, 평소보다 길거나 짧은 운전에서도 정확하게 유지됩니다. 기기가 조용할 때 갱신되므로 활성 단계 중에는 뒤처질 수 있습니다.", + "envelope_position": "곡선 {pct}%", + "aria_playground_chart3": "대화형 사이클 전력 그래프: 마우스를 올리면 시간/전력 표시, 스크롤 또는 두 손가락으로 확대/축소, 드래그하여 이동", + "device_unresolved": "사용할 수 없는 기기 ({id})", "drag_to_resize": "드래그하여 크기 조정", "actions": "작업", "activity": "활동", @@ -508,7 +525,7 @@ "profile_index": "프로필 번호 →", "profile_name": "프로필 이름", "program": "프로그램", - "program_tip": "현재 사이클에 매칭되는 프로파일을 재정의합니다. 자동 감지를 통해 통합이 최적의 일치를 자동으로 선택합니다. 자동 감지가 잘못되었거나 실행 중인 프로그램을 알 때 특정 프로그램을 고정하여 강제 일치시킵니다.", + "program_tip": "현재 사이클에 매칭되는 프로파일을 재정의합니다. 자동 감지를 통해 통합이 최적의 일치를 자동으로 선택합니다. 자동 감지가 잘못되었거나 실행 중인 프로그램을 알 때 특정 프로그램을 고정하여 강제 일치시킵니다. 기기를 작동하기 전에 하나를 선택하면 다음 사이클이 시작되는 즉시 적용됩니다.", "progress": "진행률", "raw_socket": "원시 소켓", "read_only": "읽기 전용", @@ -543,6 +560,7 @@ "stage4": "4단계 – 일치도", "status": "상태", "tail_trim": "끝부분 자르기(초)", + "tap_to_dismiss": "그래프를 탭하여 닫기", "timer_auto_pause": "자동 일시중지", "timer_min": "분", "timer_msg_placeholder": "메시지(선택사항, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "남은 시간(모델)", "pg_time_left_model_tip": "모델이 추정한 남은 시간(단계 추정 + ML 혼합)으로, 고정된 카운트다운이 아닙니다.", "phase": "단계", - "pg_mode_simulate": "시뮬레이션", "pg_mode_history": "이력 테스트", - "pg_mode_sweep": "스윕", "pg_ev_detected": "감지됨", "pg_ev_match": "매칭 확정", "pg_ev_match_changed": "매칭 변경", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "자동 감지된 대기 전력 하한값을 재정의합니다(자동의 경우 빈칸으로 두세요).", "pg_stress_toggle": "대기 상태 종료 테스트", "pg_stress_toggle_desc": "기록 종료 후 기기가 대기 전력 수준에서 계속 작동하는 상황을 시뮬레이션합니다 - WashData가 사이클을 중지하는지, 언제 중지하는지 확인합니다.", - "pg_sweep_2d": "2D 히트맵", - "pg_sweep_param_y": "두 번째 매개변수", "pg_obj_match": "매칭 정확도", "pg_obj_endtiming": "종료 타이밍 정확도", "pg_obj_falseend": "잘못된 종료율", "pg_obj_overrun": "소요 시간 편차", "pg_obj_ambiguity": "모호 비율", - "pg_best_combo": "최적 조합", "pg_mode_optimize": "최적화", "pg_group_matching": "프로그램 매칭", "pg_zoom_hint": "더블클릭하여 확대/축소 초기화", @@ -706,7 +719,12 @@ "task_history_import_apply": "사이클 가져오기", "evidence_real_cycles": "이 기기가 실행한 사이클", "evidence_reference_cycles": "커뮤니티 스토어에서 다운로드", - "evidence_backfill_cycles": "가져온 전력 이력에서 발견" + "evidence_backfill_cycles": "가져온 전력 이력에서 발견", + "zoom_hint_touch": "두 손가락으로 확대/축소", + "zoom_hint_touch_zoomed": "두 손가락으로 확대/축소 · 두 번 탭하여 초기화", + "cycles_since_service": "{since} / {total} 사이클", + "total_cycles_run": "누적 실행 사이클", + "correct_total_cycles": "누적 값 수정" }, "log": { "all_levels": "모든 레벨", @@ -756,7 +774,7 @@ "merge_cycles": "{n} 사이클 병합", "new_group": "새 프로필 그룹", "new_phase": "새로운 단계", - "process_history_msg": "매칭을 재실행하고, 제안을 새로 고치고, ML을 재훈련하고(활성화된 경우), 저장된 모든 사이클의 사이클 상태를 재계산합니다. 시간이 걸릴 수 있습니다.", + "process_history_msg": "저장된 모든 사이클에 대해 매칭을 재실행하고, 제안을 새로 고치고, ML을 재훈련하고(활성화된 경우), 기록된 에너지 가격으로 사이클 비용을 다시 계산하고, 사이클 건강을 재계산합니다. 시간이 걸릴 수 있습니다.", "process_history_ok": "처리", "process_history_title": "기록 처리", "process_recording": "프로세스 기록", @@ -789,6 +807,7 @@ "history_import": "전력 이력 가져오기" }, "msg": { + "pg_canvas_empty3": "위에서 사이클을 선택하고 실행을 눌러 시뮬레이션하세요. 그런 다음 마우스를 올려 값을 확인하고, 스크롤 또는 두 손가락으로 확대/축소하고, 드래그하여 이동할 수 있습니다.", "tail_trim_hint": "끝부분에서 제거할 초 수", "store_sibling_hint": "정확히 같은 모델로 공유된 항목이 없나요? 같은 브랜드의 비슷한 모델이 보통 좋은 출발점이 됩니다.", "store_declare_appliance": "보유한 가전제품을 WashData에 지정하면 이 탭에 다른 사용자가 공유한 프로그램과 녹음이 표시됩니다. 위에 브랜드를 입력해 둘러볼 수도 있습니다.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "일반 전력 범위 위로 약 {n}초 동안 나타났습니다.", "auto_label_intro": "일치 신뢰도가 임계값을 클리어하는 레이블이 없는 주기에 프로필을 할당합니다.", "automations_intro": "WashData는 {start} / {end} 이벤트를 발생시키고 엔티티를 노출하므로, 알림과 동작은 일반 Home Assistant 자동화로 구축하는 것이 가장 좋습니다. 이 기기를 사용하는 자동화가 아래에 표시됩니다.", - "cleanup_intro": "라벨이 붙은 모든 사이클이 중첩됩니다. 이상값을 선택하고 삭제하여 프로필을 정리하세요.", + "cleanup_intro": "라벨이 붙은 모든 사이클을 겹쳐서 보여줍니다. 이상값을 선택한 다음 라벨을 해제하거나(기록에는 남지만 더 이상 이 프로필을 형성하지 않습니다) 완전히 삭제하세요.", "clear_debug_hint": "저장된 디버그 데이터를 제거하여 여유 공간을 확보하세요.", "collecting_data": "데이터 수집 중입니다. 미세 조정을 시작하기까지 더 필요한 사이클: {need} ({current}/{min}).", "compare_overlay_profiles": "오버레이 프로필(희미함)", @@ -901,7 +920,7 @@ "preferences_admin": "환경 설정, 패널 및 액세스 제어", "preferences_adv": "환경설정", "prefs_personal": "이는 홈어시스턴트 계정에만 적용됩니다.", - "process_history_hint": "저장된 모든 사이클에 대해 매칭을 재실행하고, 튜닝 제안을 새로 고치고, ML 모델을 재훈련하고(활성화된 경우), 사이클 상태를 재계산합니다. 일괄 검토 후 실행하세요.", + "process_history_hint": "저장된 모든 사이클에 대해 매칭을 재실행하고, 튜닝 제안을 새로 고치고, ML 모델을 재훈련하고(활성화된 경우), 기록된 에너지 가격으로 사이클 비용을 다시 계산하고, 사이클 건강을 재계산합니다. 일괄 검토 후 실행하세요.", "profile_deleted": "프로필이 삭제되었습니다", "profile_poor_health_detail": "이 프로필에 할당된 주기의 모양이 일관되지 않거나 신뢰도가 낮습니다. 봉투를 재구성하거나 라벨이 붙은 주기를 검토하는 것을 고려하십시오.", "profiles_intro": "통계, 단계 및 정리를 보려면 프로필을 클릭하세요. 거의 동일한 프로그램(동일한 모양/기간, 다른 온도 또는 회전)을 그룹화하여 일치하는 프로그램을 안정적으로 선택합니다.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}'의 적합도 점수가 낮습니다 - 최근 사이클의 편차가 크거나 매칭이 약합니다. 매칭과 시간 추정의 정확도를 유지하려면 사이클을 검토하거나 프로필을 다시 기록하세요.", "advisory_shape_drift": "'{name}'이(가) 원래 전력 파형에서 크게 벗어났습니다. 시간이 지나며 기기 동작이 변했을 수 있습니다(예: 물때, 마모). 최근 사이클로 이 프로필을 다시 기록하는 것을 고려하세요.", "advisory_shape_drift_corr": "'{name}'이(가) 원래 전력 파형에서 크게 벗어났습니다(상관관계 {corr}). 시간이 지나며 기기 동작이 변했을 수 있습니다(예: 물때, 마모). 최근 사이클로 이 프로필을 다시 기록하는 것을 고려하세요.", + "advisory_unmatchable": "'{name}'은(는) 매칭될 수 없습니다. 전력 데이터가 있는 사이클이 하나도 없어 WashData가 실행 중인 사이클과 비교할 대상이 없습니다. 이 프로그램의 사이클에 라벨을 붙이거나 사이클을 기록하면 다시 매칭 대상이 됩니다.", + "advisory_unmatchable_no_duration": "'{name}'은(는) 매칭될 수 없습니다. 전력 데이터가 있는 사이클은 있지만 사용할 수 있는 소요 시간이 없어 WashData가 매칭의 기준으로 삼을 길이가 없습니다. 이 프로그램을 다시 기록하거나 예상 소요 시간을 설정하면 다시 매칭 대상이 됩니다.", "pg_no_alerts": "이번 실행에서 문제가 감지되지 않았습니다.", "pg_override_active": "편집한 설정을 현재 설정과 비교하여 사용 중", - "pg_history_intro": "최근 사이클을 실제 감지기와 매처로 다시 재생합니다. 여기에서 전후 비교를 보려면 먼저 시뮬레이션에서 감지 설정을 편집하세요.", "pg_history_empty": "실행을 눌러 사이클을 다시 재생하세요.", "pg_history_summary": "{detected}/{total} 감지됨 · {correct} 올바르게 매칭", "pg_sweep_no_metric": "이 목표를 평가하기에 데이터가 부족합니다.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "검색에 실패했습니다.", "hist_scan_expired": "해당 검색 결과는 더 이상 사용할 수 없습니다. 다시 검색해 주세요.", "hist_import_failed": "가져오기에 실패했습니다.", - "imported_history_readonly": "가져온 전력 이력에서 감지되었습니다. 프로그램 매칭에 반영되지만 통계에는 포함되지 않으며, 자르거나 분할할 수 없습니다. 라벨을 지정해 프로그램 이름을 정하세요." + "imported_history_readonly": "가져온 전력 이력에서 감지되었습니다. 프로그램 매칭에 반영되지만 통계에는 포함되지 않으며, 자르거나 분할할 수 없습니다. 라벨을 지정해 프로그램 이름을 정하세요.", + "odometer_intro": "이 누적 값은 줄어들지 않습니다. 사이클 기록을 삭제해도 값이 바뀌지 않으며, 저장되는 기록 한도를 넘어서도 계속 집계되므로 서비스 알림이 정확하게 유지됩니다.", + "odometer_correct_hint": "WashData가 실제로 일어나지 않은 작동을 기록했거나, WashData를 설치하기 전에 기기가 이미 사이클을 실행한 적이 있다면 이 값을 설정하세요.", + "no_reminders_set": "설정된 서비스 알림이 아직 없습니다.", + "toast_program_armed": "다음 사이클용 프로그램이 설정됨: {program}" }, "pg_desc": { "completion_min_seconds": "실제 사이클로 인정되는 최단 실행", @@ -1093,7 +1117,9 @@ "duration_scale": "Stage 4: 지속 시간 일치 점수가 반으로 줄어드는 로그 비율; 작을수록 엄격한 패널티(기본값 0.175)", "energy_scale": "Stage 4: 에너지 일치 점수가 반으로 줄어드는 로그 비율; 작을수록 엄격한 패널티(기본값 0.25)", "dishwasher_end_spike_quiet_release": "식기세척기: 예상 소요 시간 이후 사이클 종료 배수 대기가 해제되기 전 조용히 유지되는 초", - "smart_termination_duration_ratio": "일치된 프로그램의 예상 소요 시간 중 스마트 종료가 조기에 끝낼 수 있으려면 주기가 도달해야 하는 비율; 부하나 온도에 따라 달라지는 기기에서는 낮추십시오" + "smart_termination_duration_ratio": "일치된 프로그램의 예상 소요 시간 중 스마트 종료가 조기에 끝낼 수 있으려면 주기가 도달해야 하는 비율; 부하나 온도에 따라 달라지는 기기에서는 낮추십시오", + "anti_crease_finalize_ratio": "구김 방지 텀블 꼬리를 확정하기 전에 사이클이 도달해야 하는, 일치된 프로그램 예상 소요 시간의 비율. 텀블 꼬리가 긴 건조기에서는 낮추고 세탁기는 기본값으로 두십시오", + "curve_preroll_seconds": "중단된 시작 시도의 측정값을 사이클 곡선 앞부분에 포함하는 초 수. 0 = 끄기" }, "phase_desc": { "anti_crease": "메인 사이클 후 드럼이 간헐적으로 회전하여 의류 구김을 방지합니다.", @@ -1256,7 +1282,7 @@ "label": "종료 반복 횟수" }, "energy_price_entity": { - "doc": "kWh당 현재 전기 가격이 포함된 센서(예: 동적 요금) 아래의 고정 가격보다 우선 적용됩니다. 각 사이클은 완료 시 가격을 동결시킵니다.", + "doc": "kWh당 현재 전기 가격을 제공하는 센서(예: 동적 요금제). 반드시 가격이어야 하며, 플러그 자체의 전력이나 에너지 엔티티를 지정해서는 안 됩니다. 아래의 고정 가격보다 우선 적용되므로, 여기에 kWh 적산 계량기를 지정하면 모든 사이클이 요금 단가가 아닌 계량기 값으로 계산됩니다. 시간 가중 비용을 켜면 각 사이클이 실행된 매 순간에 적용되던 가격으로 계산되고, 끄면 완료 시점에 적용되던 가격으로 고정됩니다.", "label": "에너지 가격 엔티티" }, "energy_price_static": { @@ -1284,7 +1310,7 @@ "label": "외부 트리거 반전 (OFF에서 트리거)" }, "learning_confidence": { - "doc": "일치 점수가 이 값과 자동 라벨 신뢰도 사이에 있으면 식별된 프로그램을 확인하라는 피드백 알림이 표시됩니다. 이 점수 이하에서는 경기가 표면화되기에는 너무 불확실합니다. 자동 라벨 신뢰도 미만으로 유지되어야 합니다.", + "doc": "일치 점수가 이 값과 자동 라벨 신뢰도 사이에 있으면 식별된 프로그램을 확인하라는 피드백 알림이 표시됩니다. 이 점수 미만에서는 일치가 너무 불확실하여 표시되지 않습니다. 자동 라벨 신뢰도보다 낮게 유지해야 합니다.", "label": "학습 신뢰도" }, "linked_device": { @@ -1352,7 +1378,7 @@ "label": "알림용 HA 이벤트 실행" }, "notify_icon": { - "doc": "알림을 위한 선택적 mdi 아이콘(예: mdi:washing-machine) 플랫폼 기본값은 비워 두세요.", + "doc": "알림에 표시되는 작은 아이콘(예: mdi:washing-machine). Android는 상태 표시줄에 표시하고, iOS는 앱 아이콘 대신 표시하며 컴패니언 앱 2026.8 이상이 필요합니다. 플랫폼 기본값을 사용하려면 비워 두세요.", "label": "알림 아이콘" }, "notify_live_chronometer": { @@ -1452,7 +1478,7 @@ "label": "매칭 임계값" }, "profile_unmatch_threshold": { - "doc": "라이브 중간 사이클 경기가 이 점수 아래로 떨어지면 임시 식별이 지워집니다. 일치 임계값보다 약간 낮게 유지하면 유사성이 잠시 떨어지더라도 디스플레이가 일치하지 않는 상태로 되돌아가지 않습니다.", + "doc": "사이클 진행 중의 실시간 매칭 점수가 이 값 아래로 떨어지면 임시 식별이 해제됩니다. 매칭 임계값보다 약간 낮게 유지하여 유사도가 잠시 떨어지더라도 표시가 매칭 안 됨 상태로 되돌아가지 않도록 하세요.", "label": "언매칭 임계값" }, "progress_reset_delay": { @@ -1463,10 +1489,6 @@ "doc": "펌프가 고착되었을 가능성이 있는 것으로 표시되기 전에 펌프가 계속 작동할 수 있는 시간(초)입니다(펌프 고착 이벤트 발생).", "label": "펌프 정지 지속 시간" }, - "running_dead_zone": { - "doc": "주기가 시작된 후 이 창 내의 전력 강하는 무시됩니다. 세탁기는 찬물로 채워집니다(가열 전 0W 근처로 떨어짐). 이러한 보호 기능이 없으면 채우기 단계가 사이클 종료처럼 보입니다. 이는 데이터를 건너뛰지 않습니다. 전체 전력 추적은 T=0에서 기록됩니다. 제안 엔진은 컴퓨터의 실제 시작 패턴을 측정하고 자동으로 크기를 조정합니다.", - "label": "실행 데드존" - }, "sampling_interval": { "doc": "센서 판독 사이의 예상 시간 - 스무딩 창의 크기를 조정하고 디바운싱을 올바르게 시작하는 데 사용됩니다. 모든 센서 업데이트는 이 값에 관계없이 캡처됩니다. 다운스트림 계산만 보정합니다. 제안 엔진은 과거 주기에서 센서의 실제 케이던스를 측정하고 이를 자동으로 설정합니다.", "label": "샘플링 간격" @@ -1594,13 +1616,17 @@ "label": "전력 프로파일 간격", "doc": "EMHASS, tibber_prices 등 외부 계획 도구가 사용하는 per-profile power_profile 센서 속성(슬롯당 평균 와트 배열)의 버킷 크기입니다. 작은 버킷은 짧은 전력 급등을 선명하게 유지하고, 큰 버킷은 형태를 완만하게 합니다. 기본값 15분. 읽기 시에만 사용되며 검출에는 영향을 주지 않습니다." }, + "notify_live_silent": { + "label": "무음 라이브 업데이트", + "doc": "iOS 전용. 라이브 진행 상황을 조용히 새로 고칩니다. 각 업데이트는 소리나 진동 없이 도착하며, 배터리를 아끼는 낮은 우선순위로 전송되므로 iOS가 잠시 묶어서 전달할 수 있습니다. 사이클의 첫 업데이트(실시간 현황을 시작하는 업데이트)와 시작/완료 알림은 평소대로 알려 줍니다. 모든 업데이트마다 알림을 받으려면 이 옵션을 끄세요." + }, "notify_live_sticky": { "label": "탭 후 실시간 알림 유지", "doc": "Android 전용. 실시간 진행 알림을 고정(스티키)으로 만들어 탭해도 진행 중인 알림 스레드가 사라지지 않도록 합니다. 꺼져 있으면 탭 시 알림이 사라지는 기본 동작이 유지됩니다." }, "notify_live_click_action": { - "label": "실시간 알림 탭 대상", - "doc": "Android 전용. 실시간 진행 알림을 탭했을 때 열리는 위치(예: /lovelace/laundry 또는 전체 URL)를 앱 기본 시작 페이지 대신 지정합니다. 기본값으로 두려면 비워 두세요." + "label": "알림 탭 대상", + "doc": "이 가전제품의 알림을 탭했을 때 열리는 위치로, Android와 iOS에 모두 적용됩니다. 비워 두면 이 가전제품이 열린 WashData 패널로 이동합니다. 다른 곳으로 보내려면 대시보드 경로(예: /lovelace/laundry)나 전체 URL을 입력하고, 탭 대상을 아예 두지 않으려면 'none'을 입력하세요." }, "door_opens_at_end": { "label": "종료 시 자동으로 문 열림", @@ -1617,6 +1643,18 @@ "profile_evidence_sources": { "label": "프로그램을 형성하는 사이클", "doc": "각 프로그램의 전력 곡선을 만들고 완료된 사이클을 그것과 매칭할 때 사용할 사이클 종류입니다. 종류의 선택을 해제하면 해당 종류는 더 이상 프로그램에 반영되지 않지만 아무것도 삭제되지 않습니다. 사이클은 사이클 목록에 그대로 남아 있으며 계속 라벨을 지정하거나 삭제할 수 있습니다. 가져온 데이터를 신뢰할 수 없을 때 유용합니다. 통계에는 영향이 없습니다. 통계는 항상 이 기기가 실제로 실행한 사이클만 계산합니다. 모두 선택 해제하면 무시됩니다. 근거가 되는 사이클이 없는 프로그램은 결코 매칭될 수 없기 때문입니다." + }, + "energy_price_dynamic": { + "label": "시간 가중 비용", + "doc": "완료 시점의 가격 하나로 계산하는 대신, 사이클이 실행된 매 순간에 적용되던 가격으로 비용을 계산합니다. 가격 엔티티를 설정한 경우에만 적용됩니다(고정 가격은 변하지 않습니다). 추가 설정은 필요 없습니다. 사이클이 진행되는 동안 가격을 실시간으로 추적하며, 진단의 기록 처리를 실행하면 이전 사이클도 recorder 기록을 바탕으로 다시 계산합니다. 끄면 기존 방식대로 종료 시점의 가격 하나로 고정됩니다." + }, + "anti_crease_finalize_ratio": { + "label": "구김 방지 확정 비율", + "doc": "WashData 가 지금 보이는 저전력 텀블을 구김 방지 꼬리로 판단하고 거기서 사이클을 끝내도 되는 시점을, 일치된 프로그램의 예상 소요 시간에 대한 비율로 지정합니다. 예상 소요 시간은 해당 프로그램의 평균이므로, 센서 건조로 운전 시간이 세탁물 양에 따라 달라지는 건조기에서는 대부분의 운전이 그 98%에 도달하지 못하고 꼬리가 대체 타임아웃까지 그대로 남습니다. 기계가 실제로 멈춘 뒤 15~40분이 지나는 경우도 흔합니다. 그런 건조기에서는 이 값을 낮추십시오(예: 0.75). 세탁기는 기본값으로 두십시오. 세탁기는 사이클의 대부분을 '구김 방지 최대 전력' 아래에서 보내므로 조용한 세탁 중간 단계와 꼬리를 구분하는 것은 이 비율뿐이며, 값을 낮추면 세탁이 절반에서 잘릴 수 있습니다. 비워 두면 기본값(0.98)이 사용됩니다. 이 설정은 별도의 조기 종료 검사를 제어하는 '스마트 종료 비율'과는 다릅니다." + }, + "curve_preroll_seconds": { + "label": "곡선 프리롤", + "doc": "중단된 시작 시도에서 얻은 측정값을 사이클 곡선의 앞부분에 몇 초까지 포함할 수 있는지 지정합니다. 사이클 곡선은 보통 최종적으로 성공한 시작 시도부터 시작하므로, 프로그램 선택, 도어 잠금, 첫 급수처럼 몇 번 시도한 뒤에야 안정되는 기계에서는 실제 동작의 첫 몇 분이 모든 곡선에서 빠집니다. 기계에서 가장 긴 준비 구간보다 조금 큰 값으로 설정하십시오(대부분 300초면 충분합니다). 0이면 꺼집니다. 이 기능을 켜면 기록되는 시작 시점이 앞당겨지므로, 변경 전후에 기록된 사이클은 같은 프로그램이라도 소요 시간이 달라집니다. 오래된 사이클이 밀려날 때까지 학습된 평균값은 어긋난 상태로 유지됩니다." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "분할 중…", "deleting": "삭제 중…", "imported": "가져옴", - "preparing": "준비 중…" + "preparing": "준비 중…", + "unlabelling": "라벨 해제 중…" }, "tab": { "advanced": "고급", @@ -1808,6 +1847,7 @@ "recording_stopped": "녹음이 중지되었습니다.", "review_saved": "리뷰가 저장되었습니다.", "settings_conflicts": "저장하기 전에 강조 표시된 설정 충돌을 해결하세요.", + "settings_no_changes": "저장할 변경 사항이 없습니다", "settings_reverted": "설정이 복원되었습니다. 통합 리로드 중", "settings_saved": "설정이 저장되었습니다. 통합 다시 로드", "suggestions_applied": "제안이 적용되었습니다. 통합 다시 로드", @@ -1866,7 +1906,11 @@ "export_selective_done": "내보내기 다운로드 완료", "import_selective_done": "{profiles}개 프로필과 {cycles}개 사이클을 가져왔습니다", "hist_csv_required": "먼저 CSV 파일을 불러오거나 내용을 붙여넣으세요", - "file_read_failed": "해당 파일을 읽을 수 없습니다" + "file_read_failed": "해당 파일을 읽을 수 없습니다", + "unlabel_done": "{count}개 사이클의 라벨을 해제했습니다", + "unlabel_failed": "라벨 해제 실패: {error}", + "odometer_saved": "누적 사이클 수가 저장되었습니다", + "odometer_save_failed": "누적 값을 저장할 수 없습니다: {error}" }, "suggestion": { "both_agree": "WashData 권장", @@ -1913,10 +1957,6 @@ "higher": "높은 신뢰도 매칭만 확정합니다. 더 많은 사이클이 라벨 없이 남습니다", "lower": "더 많은 사이클을 매칭합니다. 일부가 약간 틀릴 수 있습니다" }, - "running_dead_zone": { - "higher": "대기 중 짧은 전력 스파이크를 무시합니다. 노이즈에 덜 민감합니다", - "lower": "더 짧은 전력 버스트에 반응합니다. 빠른 일시적 활동을 감지합니다" - }, "start_threshold_w": { "higher": "짧은 전력 스파이크로 인한 오동작을 방지합니다", "lower": "저전력 프로그램을 감지합니다. 노이즈에 트리거될 수 있습니다" @@ -1962,7 +2002,9 @@ "thr_batch": "{cycles} 사이클의 p05 최저 활성 전력({p05}W) 바로 위로 유지합니다. 그래서 시작을 최대한 빨리 포착하고, 정지 임계값은 기기의 최저 작동 전력보다 낮게 유지됩니다.", "tol_per_profile": "{profiles}개 프로필({cycles} 사이클)의 프로필별 시간 분산 p75. 편차가 작은 프로필은 불이익을 받지 않습니다.", "tol_pooled": "{cycles}개 최근 라벨 사이클의 통합 시간 분산 기반(p95 편차={dev}).", - "watchdog": "안전한 범위에서 최대한 낮게 유지(p95 업데이트 간격 {p95}s보다 약간 크고, 샘플링 간격 {median}s의 2배 이상, 최소 30s). 잘못된 정지 없이 멈춤을 빠르게 감지합니다." + "watchdog": "안전한 범위에서 최대한 낮게 유지(p95 업데이트 간격 {p95}s보다 약간 크고, 샘플링 간격 {median}s의 2배 이상, 최소 30s). 잘못된 정지 없이 멈춤을 빠르게 감지합니다.", + "match_interval_capped": "연속 {persistence}회 매칭이 최단 프로그램({shortest}s)의 {pct}% 안에 들어가도록 상한을 적용했습니다. 업데이트 주기(중앙값={median}s)만 고려했다면 더 긴 간격이 제안되었을 것입니다.", + "match_interval_floored": "최단 프로그램({shortest} 초)만으로 계산하면 상한이 {cap} 초가 되어 최소값 {minimum} 초보다 낮으므로 최소값에서 멈춥니다. 연속 {persistence}회 매칭에는 {budget} 초가 걸리며, 이는 해당 프로그램의 {pct}%를 넘습니다. 매칭은 측정값이 도착할 때만 실행되므로(중앙값={median} 초) 최소값으로 두어도 부담이 없습니다." }, "exclusions": { "summary": "잘못 감지된 사이클 {total}개를 제외했습니다: {parts}.", @@ -1998,10 +2040,11 @@ "date_tip": "사이클이 시작된 날짜와 시간입니다.", "duration_tip": "시작부터 종료까지의 총 사이클 실행 시간입니다.", "energy_tip": "소비된 총 에너지(kWh)입니다. 시간에 따라 전력을 적분하여 계산합니다.", - "cost_tip": "이 사이클의 에너지 비용으로, 완료 시점에 적용된 가격(에너지 x kWh당 가격)으로 고정됩니다. 값을 채우려면 설정에서 가격을 지정하세요.", + "cost_tip": "이 사이클의 에너지 비용으로, 완료 시점에 고정됩니다. 시간 가중 비용을 켜면 전력 곡선이 각 시점에 적용되던 가격으로 계산되고, 끄면 에너지 x 종료 시점의 가격 하나로 계산됩니다. 값을 채우려면 설정에서 가격을 지정하세요.", "confidence_tip": "프로필 매칭 신뢰도(0-100%)입니다. 사이클 전력 곡선이 식별된 프로그램과 얼마나 근접하게 일치했는지를 나타냅니다.", "health_tip": "ML 사이클 건강(높을수록 좋음)입니다. 검사하고 검토하려면 사이클을 클릭하세요.", - "flags_tip": "사이클의 검토, 이상, 소스 플래그. 아이콘에 마우스를 올리면 세부 정보가 표시됩니다." + "flags_tip": "사이클의 검토, 이상, 소스 플래그. 아이콘에 마우스를 올리면 세부 정보가 표시됩니다.", + "cost_dynamic_tip": "시간 가중: 사이클 진행 중 적용되던 가격으로 계산됨(실효 {price} {cur}/kWh)." }, "stat": { "avg": "평균", @@ -2028,9 +2071,9 @@ "finished": "사이클이 종료 상태에 도달하여 끝났습니다." }, "store": { + "search_ph2": "제조사 또는 모델명 검색…", "your_model_tip": "설정에서 지정한 가전제품입니다", "your_model": "내 기기", - "search_brand_ph": "브랜드로 검색…", "programs_count": "프로그램: {n}", "browse": "탐색", "device": "기기", @@ -2075,7 +2118,8 @@ "golden": "재처리: 참조 사이클 보완", "suggestions": "재처리: 제안", "ml_training": "재처리: ML 훈련", - "health": "재처리: 사이클 건강" + "health": "재처리: 사이클 건강", + "costs": "재처리: 에너지 비용" }, "pg_sweep": { "optimize": "최적화: {param}" diff --git a/custom_components/ha_washdata/translations/panel/lt.json b/custom_components/ha_washdata/translations/panel/lt.json index cdd20a40..bc1bff25 100644 --- a/custom_components/ha_washdata/translations/panel/lt.json +++ b/custom_components/ha_washdata/translations/panel/lt.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Pabaigos link ši programa maždaug {mins} min nutyla, tada prieš baigdama {secs} s naudoja apie {watts} W. Pastebėta {seen} iš {measured} išmatuotų ciklų: prietaisas tai daro ne kiekvieną kartą.", + "quiet_tail": "tyli pabaiga ~{mins} min", "artifact_tip": "Aptikta {n} anomalijų (pvz., durys atidarytos ciklo viduryje) – atidarykite, kad pamatytumėte juos diagramoje", "auto": "(aptikta automatiškai)", "built_in_tag": "įmontuotas", @@ -63,7 +65,7 @@ "needs_review": "Reikia peržiūros", "overrun": "Bėgo ilgiau nei įprastai", "poor_fit": "prastai tinka", - "poor_fit_tip": "Nenuosekli rungtynių istorija – apsvarstykite galimybę atkurti šį profilį", + "poor_fit_tip": "Nenuosekli atitikčių istorija – apsvarstykite galimybę atkurti šį profilį", "restart_gap_tip": "{n} HA paleidimo iš naujo spraga: galios kreivė turi skylę", "reviewed": "Peržiūrėta", "steady": "→ pastovus", @@ -77,11 +79,14 @@ "approved": "Patvirtinta", "awaiting": "Laukiama patvirtinimo", "imported_tip": "Importuota iš bendruomenės parduotuvės. Naudojama tik atitikčiai, neįskaičiuojama į statistiką.", + "unmatchable": "negalima atpažinti", "not_importable": "čia nepasiekiama", "exists": "jau yra", - "backfilled_tip": "Aptikta importuotoje galios istorijoje. Turi įtakos tik programų atpažinimui, neįskaičiuojama į statistiką." + "backfilled_tip": "Aptikta importuotoje galios istorijoje. Turi įtakos tik programų atpažinimui, neįskaičiuojama į statistiką.", + "armed": "(taikoma kitam ciklui)" }, "btn": { + "reset_zoom": "Atstatyti mastelį", "set_brand_model": "Nustatyti prekės ženklą ir modelį", "refresh_catalog": "Atnaujinti katalogą", "add_device": "+ Pridėti įrenginį", @@ -158,7 +163,7 @@ "pg_publish_one": "Įrašyti „{label}“ į integracijos nustatymus", "play": "Leisti", "process": "Apdoroti", - "process_history": "Apdorokite dabar", + "process_history": "Apdoroti dabar", "process_recording": "Išsaugoti įrašą", "process_tip": "Išsaugoti įrašytą seką kaip naują arba esamą profilį", "rebuild": "Atstatyti vokus", @@ -215,6 +220,8 @@ "trim": "Apkarpyti", "trim_split": "Apkarpyti / padalinti", "undo": "Anuliuoti", + "unlabel_selected": "Panaikinti pasirinktų žymėjimą ({n})", + "unlabel_selected_tip": "Pašalina profilio žymėjimą iš pasirinktų ciklų. Jie lieka jūsų istorijoje ir juos vėliau galima pažymėti iš naujo - tiesiog nustoja formuoti šį profilį.", "use": "Naudokite", "wipe_all": "Išvalykite visus duomenis", "delete_selected": "Ištrinti pasirinktus ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Taikyti geriausią", "pg_apply_to_settings": "Įrašyti į nustatymus", "pg_apply_to_settings_tip": "Kopijuoti čia redaguotas reikšmes į aktyvius šio įrenginio nustatymus", - "search": "Ieškoti", "import": "Importuoti", "disconnect": "Atjungti", "connect_github": "Prijungti GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Nuskaityti iš Home Assistant", "hist_scan": "Ieškoti ciklų", "hist_import_n": "Importuoti ciklus ({n})", - "hist_goto_cycles": "Parodyti ciklus" + "hist_goto_cycles": "Parodyti ciklus", + "save_total_cycles": "Išsaugoti bendrą skaičių" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Prieš išsaugant ištaisykite konfliktus", "attn_title": "Nustatymų konfliktai: {n}", + "price_entity": { + "power_sensor": "Tai yra įrenginio Galios jutiklis, o ne kaina. Kainos objektas turi pirmenybę prieš toliau nurodytą statinę kainą, todėl ciklai būtų įkainojami pagal vatus už kWh - išvalykite šį lauką.", + "energy_sensor": "Tai yra įrenginio Energijos skaitiklis, o ne kaina. Kainos objektas turi pirmenybę prieš toliau nurodytą statinę kainą, todėl ciklai būtų įkainojami pagal skaitiklio rodmenį už kWh - išvalykite šį lauką.", + "device_class": "Šio jutiklio klasė yra {dc}, o ne kaina už kWh. Kainos objektas turi pirmenybę prieš toliau nurodytą statinę kainą - išvalykite šį lauką arba nurodykite tarifo jutiklį.", + "unit": "Šio jutiklio matavimo vienetas yra {unit}, o tai nėra kaina už kWh. Kainos objektas turi pirmenybę prieš toliau nurodytą statinę kainą - išvalykite šį lauką arba nurodykite tarifo jutiklį." + }, "settings_banner": "Nustatymų konfliktai: {n}. Patikrinkite pažymėtas sekcijas ir ištaisykite jas prieš išsaugant.", "settings_banner_btn": "Eiti į pirmą", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Nustatymų šaltinis", "phase_catalog": "Fazių katalogas", "playground": "Bandymų aikštelė", - "process_history": "Proceso istorija", + "process_history": "Apdoroti istoriją", "settings_history": "Nustatymų istorija", "status": "Būsena", "status_graph": "Būsenos grafikas", @@ -360,12 +373,12 @@ "wipe_history": "Išvalyti istoriją", "recommendations": "Rekomendacijos ({n})", "pg_detection_params": "Aptikimo nustatymai", - "pg_timeline": "Įvykių chronologija", "pg_outcome": "Simuliacijos rezultatas", "pg_across_cycles": "Per visus jūsų ciklus", "community_store": "Bendruomenės parduotuvė", "online_account": "Bendruomenės parduotuvė ir interneto funkcijos", - "import_power_history": "Galios istorijos importavimas" + "import_power_history": "Galios istorijos importavimas", + "service_status": "Aptarnavimo būsena" }, "health": { "fair": "Priimtina profilio kokybė", @@ -373,6 +386,10 @@ "poor": "⚠ Prasta profilio kokybė" }, "lbl": { + "envelope_position_tip": "Padėtis atitinkančios programos įrašytoje kreivėje, {pct}%. Nustatoma lyginant šio ciklo formą su profiliu, o ne skaičiuojant laiką, todėl lieka teisinga, kai ciklas trunka ilgiau ar trumpiau nei įprastai. Atnaujinama, kai prietaisas tyli, todėl aktyvios fazės metu gali vėluoti.", + "envelope_position": "kreivė {pct}%", + "aria_playground_chart3": "Interaktyvus ciklo galios grafikas: užveskite žymeklį, kad nuskaitytumėte laiką/galią, slinkite arba priartinkite dviem pirštais, vilkite, kad pastumtumėte", + "device_unresolved": "Nepasiekiamas įrenginys ({id})", "drag_to_resize": "Vilkite, kad pakeistumėte dydį", "pg_anti_wrinkle": "Apsauga nuo raukšlių", "actions": "Veiksmai", @@ -508,7 +525,7 @@ "profile_index": "profilio numeris →", "profile_name": "Profilio pavadinimas", "program": "Programa", - "program_tip": "Pakeiskite, kuris profilis atitinka dabartinį ciklą. Automatinis aptikimas leidžia integracijai automatiškai pasirinkti geriausią atitikmenį. Prisegkite konkretų programą, kad jis būtų priverstinai parinktas, kai automatinis aptikimas klysta arba žinote, kas veikia.", + "program_tip": "Pakeiskite, kuris profilis atitinka dabartinį ciklą. Automatinis aptikimas leidžia integracijai automatiškai pasirinkti geriausią atitikmenį. Prisegkite konkretų programą, kad jis būtų priverstinai parinktas, kai automatinis aptikimas klysta arba žinote, kas veikia. Pasirinkite programą prieš paleisdami prietaisą ir ji bus pritaikyta vos prasidėjus kitam ciklui.", "progress": "Pažanga", "raw_socket": "Neapdorotas lizdas", "read_only": "Tik skaityti", @@ -534,7 +551,7 @@ "settings_basic": "Pagrindiniai", "settings_detail_level": "Nustatymų detalumo lygis", "settings_overrides": "Nustatymų nepaisymai", - "show_debug": "Rodyti tiesioginio rungtynių derinimo kortelę būsenos puslapyje (pasitikėjimas, dviprasmiškumas, geriausi kandidatai)", + "show_debug": "Rodyti tiesioginės atitikties derinimo kortelę būsenos puslapyje (pasitikėjimas, dviprasmiškumas, geriausi kandidatai)", "show_expected": "Rodyti numatomą kreivės perdangą (atitinkantį profilį, oranžinė)", "show_raw": "Rodyti neapdoroto lizdo perjungiklį tiesioginiame galios grafike", "sparkline": "Naujausia ciklų trukmės tendencija", @@ -543,6 +560,7 @@ "stage4": "4 etapas – atitikimas", "status": "Būsena", "tail_trim": "Apkarpymas nuo galo (s)", + "tap_to_dismiss": "palieskite grafiką, kad uždarytumėte", "timer_auto_pause": "Automatinis pristabdymas", "timer_min": "min", "timer_msg_placeholder": "Pranešimas (pasirenkama, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Liko laiko (modelis)", "pg_time_left_model_tip": "Modelio įvertintas likęs laikas (fazės vertintuvas + ML derinys), ne statinis atgalinis skaičiavimas", "phase": "Fazė", - "pg_mode_simulate": "Simuliuoti", "pg_mode_history": "Bandyti su istorija", - "pg_mode_sweep": "Skenavimas", "pg_ev_detected": "Aptikta", "pg_ev_match": "Atitiktis patvirtinta", "pg_ev_match_changed": "Atitiktis pasikeitė", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Perrašyti automatiškai aptiktą budėjimo lygio apatinę ribą (palikite tuščią automatiniam).", "pg_stress_toggle": "Testuoti budėjimo nutraukimą", "pg_stress_toggle_desc": "Imituoja prietaisą, kuris po įrašymo pasibaigimo lieka budėjimo suvartojimo lygyje - parodo, ar ir kada WashData sustabdys ciklą.", - "pg_sweep_2d": "2D šilumos žemėlapis", - "pg_sweep_param_y": "Antras parametras", "pg_obj_match": "Atitikties tikslumas", "pg_obj_endtiming": "Pabaigos laiko tikslumas", "pg_obj_falseend": "Klaidingų pabaigų dažnis", "pg_obj_overrun": "Trukmės nuokrypis", "pg_obj_ambiguity": "Neaiškumų dažnis", - "pg_best_combo": "geriausias derinys", "pg_mode_optimize": "Optimizuoti", "pg_group_matching": "Programų atitiktis", "pg_zoom_hint": "dukart spustelėkite, kad atstatytumėte mastelį", @@ -706,7 +719,12 @@ "task_history_import_apply": "Importuojami ciklai", "evidence_real_cycles": "Šio prietaiso atlikti ciklai", "evidence_reference_cycles": "Atsisiųsti iš bendruomenės parduotuvės", - "evidence_backfill_cycles": "Rasti importuotoje galios istorijoje" + "evidence_backfill_cycles": "Rasti importuotoje galios istorijoje", + "zoom_hint_touch": "priartinkite dviem pirštais", + "zoom_hint_touch_zoomed": "priartinkite dviem pirštais · dukart palieskite atstatymui", + "cycles_since_service": "{since} / {total} ciklų", + "total_cycles_run": "ciklų iš viso", + "correct_total_cycles": "Pataisyti bendrą skaičių" }, "log": { "all_levels": "Visi lygiai", @@ -756,7 +774,7 @@ "merge_cycles": "Sujungti {n} ciklus", "new_group": "Nauja profilių grupė", "new_phase": "Naujas etapas", - "process_history_msg": "Pakartotinai paleisti atitikimą, atnaujinti pasiūlymus, iš naujo apmokyti ML (jei įjungta) ir perskaičiuoti ciklų sveikatą visuose saugomuose cikluose. Tai gali užtrukti.", + "process_history_msg": "Pakartotinai paleisti atitikimą, atnaujinti pasiūlymus, iš naujo apmokyti ML (jei įjungta), perskaičiuoti ciklų kainą pagal įrašytas energijos kainas ir perskaičiuoti ciklų sveikatą visuose saugomuose cikluose. Tai gali užtrukti.", "process_history_ok": "Apdoroti", "process_history_title": "Apdoroti istoriją", "process_recording": "Proceso įrašymas", @@ -789,6 +807,7 @@ "history_import": "Galios istorijos importavimas" }, "msg": { + "pg_canvas_empty3": "Pasirinkite ciklą aukščiau ir paspauskite Vykdyti, kad jį simuliuotumėte. Tada užveskite žymeklį, kad nuskaitytumėte reikšmes, slinkite arba priartinkite dviem pirštais ir vilkite, kad pastumtumėte.", "tail_trim_hint": "Kiek sekundžių pašalinti nuo galo", "store_sibling_hint": "Jūsų tiksliam modeliui nieko nebendrinta? Artimas to paties prekės ženklo modelis paprastai yra gera pradžia.", "store_declare_appliance": "Nurodykite WashData, kurį prietaisą turite, ir šiame skirtuke bus rodomos kitų jam bendrintos programos bei įrašai. Taip pat galite viršuje įrašyti prekės ženklą ir tiesiog pasižiūrėti.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Viršijo įprastą galios juostą maždaug {n}s.", "auto_label_intro": "Priskirkite profilius nepažymėtiems ciklams, kurių atitikties patikimumas pašalina slenkstį.", "automations_intro": "WashData paleidžia {start} / {end} įvykius ir atskleidžia esybes, todėl pranešimai ir veiksmai geriausiai kuriami kaip įprasti Home Assistant automatizavimai. Žemiau rodomi automatizavimai, naudojantys šį prietaisą.", - "cleanup_intro": "Kiekvienas pažymėtas ciklas padengtas. Pažymėkite nuokrypius ir ištrinkite, kad išvalytumėte profilį.", + "cleanup_intro": "Visi pažymėti ciklai rodomi vienas ant kito. Pažymėkite nuokrypius ir panaikinkite jų žymėjimą (jie liks istorijoje, bet nebeformuos šio profilio) arba ištrinkite juos visiškai.", "clear_debug_hint": "Pašalinkite saugomus derinimo duomenis, kad atlaisvintumėte vietos.", "collecting_data": "Renkami duomenys. Dar reikia ciklų, kad galėtų prasidėti tikslus derinimas: {need} ({current}/{min}).", "compare_overlay_profiles": "Perdangos profiliai (blankūs)", @@ -901,7 +920,7 @@ "preferences_admin": "Parinktys, skydelis ir prieigos valdymas", "preferences_adv": "Parinktys", "prefs_personal": "Tai galioja tik jūsų namų asistento paskyrai.", - "process_history_hint": "Pakartotinai paleisti atitikimą visiems saugomiems ciklams, atnaujinti derinimo pasiūlymus, iš naujo apmokyti ML modelius (jei įjungta) ir perskaičiuoti ciklų sveikatą. Paleiskite tai po peržiūrų partijos.", + "process_history_hint": "Pakartotinai paleisti atitikimą visiems saugomiems ciklams, atnaujinti derinimo pasiūlymus, iš naujo apmokyti ML modelius (jei įjungta), perskaičiuoti ciklų kainą pagal įrašytas energijos kainas ir perskaičiuoti ciklų sveikatą. Paleiskite tai atlikę keletą peržiūrų.", "profile_deleted": "Profilis ištrintas", "profile_poor_health_detail": "Šiam profiliui priskirtų ciklų formos yra nenuoseklios arba jų patikimumas yra mažas. Apsvarstykite galimybę atkurti voką arba peržiūrėti pažymėtus ciklus.", "profiles_intro": "Spustelėkite profilį, kad pamatytumėte statistiką, etapus ir valymą. Grupuokite beveik identiškas programas (ta pati forma / trukmė, skirtinga temperatūra arba sukimas), kad suderinus būtų patikimai parenkamos tarp jų.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' turi žemą atitikties įvertį - naujausi jo ciklai labai skiriasi arba silpnai atpažįstami. Peržiūrėkite jo ciklus arba įrašykite profilį iš naujo, kad atpažinimas ir laiko įverčiai išliktų tikslūs.", "advisory_shape_drift": "'{name}' smarkiai nukrypo nuo pradinės galios kreivės. Prietaiso veikimas laikui bėgant galėjo pasikeisti (pvz., kalkės, nusidėvėjimas). Apsvarstykite galimybę įrašyti šį profilį iš naujo pagal naujausius ciklus.", "advisory_shape_drift_corr": "'{name}' smarkiai nukrypo nuo pradinės galios kreivės (koreliacija {corr}). Prietaiso veikimas laikui bėgant galėjo pasikeisti (pvz., kalkės, nusidėvėjimas). Apsvarstykite galimybę įrašyti šį profilį iš naujo pagal naujausius ciklus.", + "advisory_unmatchable": "'{name}' niekada nebus atpažintas: už jo nėra nė vieno ciklo su galios duomenimis, todėl WashData neturi su kuo palyginti vykstančio ciklo. Pažymėkite kurį nors ciklą šia programa (arba įrašykite vieną), kad profilis vėl būtų atpažįstamas.", + "advisory_unmatchable_no_duration": "'{name}' niekada nebus atpažintas: jis turi ciklą su galios duomenimis, bet be tinkamos trukmės, todėl WashData neturi trukmės, pagal kurią galėtų įvertinti atitiktį. Įrašykite šią programą iš naujo arba nurodykite numatomą jos trukmę, kad profilis vėl būtų atpažįstamas.", "pg_no_alerts": "Šiame paleidime problemų neaptikta.", "pg_override_active": "Naudojami jūsų redaguoti nustatymai, palyginti su dabartiniais", - "pg_history_intro": "Iš naujo paleiskite savo naujausius ciklus per tikrą detektorių ir atitikimo variklį. Pirmiausia redaguokite aptikimo nustatymą režime Simuliuoti, kad čia pamatytumėte prieš/po palyginimą.", "pg_history_empty": "Paspauskite Vykdyti, kad iš naujo paleistumėte savo ciklus.", "pg_history_summary": "{detected}/{total} aptikta · {correct} teisingai suderinta", "pg_sweep_no_metric": "Nepakanka duomenų šiam tikslui įvertinti.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Skenavimas nepavyko.", "hist_scan_expired": "To skenavimo rezultatų jau nėra. Nuskenuokite dar kartą.", "hist_import_failed": "Importavimas nepavyko.", - "imported_history_readonly": "Aptikta importuotoje galios istorijoje. Tai daro įtaką programų atpažinimui, bet neįskaičiuojama į jūsų statistiką, o ciklo negalima apkarpyti ar padalyti. Paspauskite „Pažymėti“, kad nurodytumėte programą." + "imported_history_readonly": "Aptikta importuotoje galios istorijoje. Tai daro įtaką programų atpažinimui, bet neįskaičiuojama į jūsų statistiką, o ciklo negalima apkarpyti ar padalyti. Paspauskite „Pažymėti“, kad nurodytumėte programą.", + "odometer_intro": "Šis bendras skaičius tik didėja. Ciklo įrašo ištrynimas jo nepakeičia, o skaičiavimas tęsiamas ir pasiekus išsaugotos istorijos ribą, todėl aptarnavimo priminimai lieka tikslūs.", + "odometer_correct_hint": "Nustatykite tai, jei WashData užregistravo ciklą, kurio iš tikrųjų nebuvo, arba jei prietaisas jau buvo atlikęs ciklus prieš WashData įdiegimą.", + "no_reminders_set": "Kol kas nenustatyta jokių aptarnavimo priminimų.", + "toast_program_armed": "Programa nustatyta kitam ciklui: {program}" }, "phase_desc": { "anti_crease": "Po pagrindinio ciklo būgnas intermitiškai sukasi, kad išvengtų audinių raukšlių.", @@ -1161,6 +1185,10 @@ "label": "Pasyvaus džiovinimo tylusis atleidimas", "doc": "Kai ciklas viršija numatytą trukmę, kiek laiko indaplovė turi išlikti tyli (žemiau Sustabdymo slenkstis) prieš tai, kai WashData nustoja laukti galutinio vandens išleidimo ir užbaigia ciklą. Padidinkite, jei jūsų aparatas turi ilgą tylią džiovinimo fazę prieš vėlyvą galutinį vandens išleidimą, kuris praleidžiamas - platesnis langas leidžia išmoktai trukmei sekti sezoninį poslinkį (šaltesnis įleidžiamas vanduo = ilgesni ciklai), užuot užsifiksavus ties senuoju vidurkiu. Jis gali tik sutrumpinti laukimą, palyginti su vidiniu 30 minučių pabaigos smailės ribojimu, bet niekada jo nepailgina." }, + "anti_crease_finalize_ratio": { + "doc": "Kaip toli į atitiktos programos numatytą trukmę ciklas turi būti pažengęs, kad WashData galėtų nuspręsti, jog matomas tylus būgno sukimas yra apsaugos nuo raukšlių uodega, ir ties ta vieta užbaigti ciklą. Numatyta trukmė yra programos vidurkis, todėl džiovyklėje, kurios jutiklinio džiovinimo trukmė priklauso nuo apkrovos, dauguma paleidimų niekada nepasiekia 98% šio vidurkio, o uodega užsilaiko iki atsarginio skirtojo laiko - dažnai nuo 15 iki 40 minučių po to, kai mašina iš tikrųjų sustojo. Tokioje džiovyklėje sumažinkite šią vertę (pvz., 0,75). Skalbyklėms palikite numatytąją vertę: skalbyklė didžiąją ciklo dalį praleidžia žemiau nustatymo Maks. apsaugos nuo raukšlių galia, todėl ši dalis yra vienintelis dalykas, skiriantis tylią skalbimo vidurio fazę nuo uodegos, o jos sumažinimas gali perpus sutrumpinti skalbimą. Palikite tuščią numatytajai vertei (0,98). Tai kitas nustatymas nei Išmaniojo užbaigimo santykis, kuris valdo atskirą ankstyvo užbaigimo patikrą.", + "label": "Apsaugos nuo raukšlių užbaigimo santykis" + }, "anti_wrinkle_enabled": { "doc": "Atpažinkite trumpus mažos galios būgninius impulsus, kuriuos džiovyklė skleidžia po pagrindinės šildymo fazės, ir laikykite juos prijungtus prie baigto ciklo, o ne skaitykite juos kaip naujus ciklus.", "label": "Įjungti apsaugą nuo raukšlių" @@ -1193,6 +1221,10 @@ "doc": "Trumpesni nei šis ciklai atmetami kaip vaiduokliniai ciklai (bandomieji važiavimai, atidarant dureles, norint įdėti kojines).", "label": "Min. ciklo trukmė" }, + "curve_preroll_seconds": { + "doc": "Kiek sekundžių rodmenų iš nutrauktų paleidimo bandymų WashData gali perkelti į ciklo kreivės pradžią. Ciklo kreivė paprastai prasideda nuo to paleidimo bandymo, kuris galiausiai pavyksta, todėl mašinoje, kuri prieš įsibėgėdama kelis kartus pabando - programos pasirinkimas, durelių užraktas, pirmasis vandens pripildymas - kiekvienoje kreivėje trūksta pirmųjų tikros veiklos minučių. Nustatykite šią vertę šiek tiek didesnę už ilgiausią tokią jūsų mašinos įsibėgėjimo trukmę (300 s daugumai yra su kaupu). 0 išjungia šią funkciją. Atminkite, kad ją įjungus įrašyta pradžia paankstinama, todėl ciklai, įrašyti prieš pakeitimą ir po jo, tai pačiai programai turi skirtingas trukmes; išmokti vidurkiai svyruos, kol senesni ciklai bus išstumti.", + "label": "Kreivės įžanginės sekundės" + }, "delay_confirm_seconds": { "doc": "Maitinimas turi likti budėjimo režimo juostoje tiek laiko, kol prietaisas bus traktuojamas kaip laukiantis įsijungimo, o ne kaip veikiantis.", "label": "Patvirtinimo sekundės" @@ -1230,7 +1262,7 @@ "label": "Pabaigos pakartojimų sk." }, "energy_price_entity": { - "doc": "Jutiklis su esama elektros kaina už kWh (pvz., dinaminis tarifas). Pirmenybė teikiama žemiau nurodytai statinei kainai. Kiekvienas ciklas užšaldo kainą, galiojančią jam pasibaigus.", + "doc": "Jutiklis su esama elektros kaina už kWh (pvz., dinaminis tarifas). Tai turi būti kaina, o ne paties kištuko galios ar energijos objektas: ši reikšmė turi pirmenybę prieš toliau nurodytą statinę kainą, todėl čia nurodytas kWh skaitiklis kiekvieną ciklą įkainoja pagal skaitiklio rodmenį, o ne pagal jūsų tarifą. Kai įjungta „Pagal laiką svertinė kaina“, kiekvienas ciklas įkainojamas pagal kainą, galiojusią kiekvieną jo veikimo akimirką; priešingu atveju jam užfiksuojama kaina, galiojusi jam pasibaigus.", "label": "Energijos kainos objektas" }, "energy_price_static": { @@ -1258,7 +1290,7 @@ "label": "Invertuoti išorinį trigerį (OFF)" }, "learning_confidence": { - "doc": "Jei atitikties rezultatas patenka tarp šio ir automatinio etiketės pasitikėjimo, atsiliepimo pranešime prašoma patikrinti identifikuotą programą. Žemiau šio rezultato rungtynės yra per daug neaiškios, kad būtų galima pamatyti. Turi būti žemiau automatinio etiketės patikimumo.", + "doc": "Jei atitikties balas patenka tarp šio ir Automatinio žymėjimo pasitikėjimo, atsiliepimo pranešime prašoma patikrinti identifikuotą programą. Žemiau šio balo atitiktis yra per daug neaiški, kad būtų rodoma. Turi būti mažesnis už Automatinio žymėjimo pasitikėjimą.", "label": "Mokymosi pasitikėjimas" }, "linked_device": { @@ -1326,7 +1358,7 @@ "label": "Paleisti HA įvykius pranešimams" }, "notify_icon": { - "doc": "Pasirenkama pranešimo mdi piktograma (pvz., mdi:skalbimo mašina). Numatytajai platformai palikite tuščią.", + "doc": "Maža piktograma, rodoma pranešime (pvz., mdi:washing-machine). Android ją piešia būsenos juostoje; iOS ją rodo vietoj programos piktogramos ir tam reikia papildomos programos 2026.8 arba naujesnės versijos. Palikite tuščią, kad būtų naudojama numatytoji platformos piktograma.", "label": "Pranešimo piktograma" }, "notify_live_chronometer": { @@ -1426,7 +1458,7 @@ "label": "Atitikimo slenkstis" }, "profile_unmatch_threshold": { - "doc": "Jei tiesioginės ciklo vidurio rungtynės nukrenta žemiau šio balo, preliminarus identifikavimas išvalomas. Laikykite jį šiek tiek žemiau atitikties slenksčio, kad trumpas panašumas nesukeltų ekrano į neprilygstamą.", + "doc": "Jei tiesioginės atitikties balas ciklo viduryje nukrenta žemiau šio balo, preliminarus identifikavimas išvalomas. Laikykite jį šiek tiek žemiau Atitikimo slenksčio, kad trumpas panašumo sumažėjimas negrąžintų ekrano į būseną be atitikties.", "label": "Neatitikimo slenkstis" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Po kelių sekundžių siurblys gali veikti nepertraukiamai, kol jis bus pažymėtas kaip galimai užstrigęs (įjungiamas įstrigusio siurblio įvykis).", "label": "Siurblio įstrigimo trukmė" }, - "running_dead_zone": { - "doc": "Prasidėjus ciklui, maitinimo kritimai šiame lange nepaisomi. Skalbimo mašinos prisipildo šalto vandens (prieš kaitinant nukrenta beveik 0 W) – be šios apsaugos ta užpildymo fazė atrodo kaip ciklo pabaiga. Tai NEpraleidžia duomenų: visos galios sekimas įrašomas nuo T=0. Pasiūlymo variklis matuoja tikrąjį jūsų mašinos paleidimo modelį ir automatiškai nustato jo dydį.", - "label": "Veikimo mirusi zona" - }, "sampling_interval": { "doc": "Numatomas laikas tarp jutiklio rodmenų – naudojamas išlyginimo lango dydžiui nustatyti ir tinkamai pradėti atšokti. Kiekvienas jutiklio atnaujinimas fiksuojamas neatsižvelgiant į šią reikšmę; tai tik kalibruoja tolesnius skaičiavimus. Pasiūlymų variklis matuoja tikrąją jutiklio kadenciją iš praėjusių ciklų ir nustato ją automatiškai.", "label": "Mėginių ėmimo intervalas" @@ -1568,13 +1596,17 @@ "label": "Galios profilio intervalas", "doc": "Kibirio dydis profilio power_profile jutiklio atributui (plokščias per laiko atkarpas vidutinių vatų masyvas, naudojamas išorinių planavimo sistemų, pvz., EMHASS ir tibber_prices). Mažesni kibirai išlaiko trumpus galios šuolius aštresniais; didesni kibirai lygina formą. Numatytasis 15 min. Tik skaitymo metu; neturi įtakos aptikimui." }, + "notify_live_silent": { + "label": "Tylūs tiesioginiai atnaujinimai", + "doc": "Tik iOS. Tiesioginė eiga atnaujinama tyliai: kiekvienas atnaujinimas gaunamas be garso ir vibracijos, žemesniu, bateriją tausojančiu prioritetu, kurį iOS gali trumpam sugrupuoti. Pirmasis ciklo atnaujinimas (tas, kuris paleidžia Live Activity) bei pradžios ir pabaigos pranešimai ir toliau praneša įprastai. Išjunkite, jei norite gauti pranešimą apie kiekvieną atnaujinimą." + }, "notify_live_sticky": { "label": "Išlaikyti tiesioginį pranešimą po paspaudimo", "doc": "Tik Android. Padaryti tiesioginį pažangos pranešimą patvariu (klijuojamu), kad paspaudus nebus atsisakyta vykstančios gijos. Išjungus paliekama numatytoji elgsena, kai paspaudus pranešimas atsisakomas." }, "notify_live_click_action": { - "label": "Tiesioginio pranešimo paspaudimo tikslas", - "doc": "Tik Android. Kur atidaroma paspaudus tiesioginės pažangos pranešimą (pvz., /lovelace/laundry arba visas URL), o ne programos pradžios puslapis. Palikite tuščią, kad naudotumėte numatytąjį." + "label": "Pranešimo paspaudimo tikslas", + "doc": "Kur atidaroma paspaudus šio prietaiso pranešimą, tiek Android, tiek iOS. Palikus tuščią, atidaromas WashData skydelis su šiuo prietaisu. Įveskite prietaisų skydelio kelią (pvz., /lovelace/laundry) arba visą URL, kad nukreiptumėte kitur, arba „none“, kad paspaudimas nieko neatidarytų." }, "door_opens_at_end": { "label": "Durys automatiškai atsidaromos pabaigoje", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Ciklai, formuojantys programą", "doc": "Kurie ciklai naudojami kiekvienos programos galios kreivei sudaryti ir baigtam ciklui su ja palyginti. Atžymėjus tam tikrą rūšį, ji nebeturi įtakos jūsų programoms, tačiau nieko neištrinama - ciklai lieka sąraše Ciklai ir juos vis tiek galima pažymėti arba ištrinti. Naudinga, jei nepasitikite importuotais duomenimis. Statistikai tai neturi įtakos: joje visada įskaičiuojami tik tie ciklai, kuriuos šis prietaisas tikrai atliko. Visų žymių pašalinimas ignoruojamas, nes programa be jokių ciklų niekada negalėtų būti atpažinta." + }, + "energy_price_dynamic": { + "label": "Pagal laiką svertinė kaina", + "doc": "Kiekvienas ciklas įkainojamas pagal kainą, galiojusią kiekvieną jo veikimo akimirką, o ne pagal vieną kainą, galiojusią jam pasibaigus. Taikoma tik kainos objektui (statinė kaina nekinta). Papildomai nieko nustatyti nereikia: ciklui veikiant kaina sekama tiesiogiai, o skiltyje Diagnostika esantis veiksmas Apdoroti istoriją perskaičiuoja senesnių ciklų kainą pagal recorder duomenis. Išjungta = įprastas veikimas, viena kaina, užfiksuota pabaigoje." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Padalijama…", "deleting": "Trinama…", "imported": "Importuota", - "preparing": "Ruošiama…" + "preparing": "Ruošiama…", + "unlabelling": "Naikinamas žymėjimas…" }, "tab": { "advanced": "Išplėstinė", @@ -1778,6 +1815,7 @@ "recording_stopped": "Įrašymas sustabdytas", "review_saved": "Apžvalga išsaugota", "settings_conflicts": "Prieš išsaugojant ištaisykite paryškintas nustatymų nesuderinamumus.", + "settings_no_changes": "Nėra pakeitimų, kuriuos reikia išsaugoti", "settings_reverted": "Nustatymai atkurti; integracijos perkrovimas", "settings_saved": "Nustatymai išsaugoti; integracijos perkrovimas", "suggestions_applied": "Pasiūlymai pritaikyti; integracijos perkrovimas", @@ -1836,7 +1874,11 @@ "export_selective_done": "Eksportas atsisiųstas", "import_selective_done": "Importuota {profiles} profilių ir {cycles} ciklų", "hist_csv_required": "Pirmiausia įkelkite CSV failą arba įklijuokite jo turinį", - "file_read_failed": "Nepavyko nuskaityti to failo" + "file_read_failed": "Nepavyko nuskaityti to failo", + "unlabel_done": "Panaikintas žymėjimas ciklams: {count}", + "unlabel_failed": "Nepavyko panaikinti žymėjimo: {error}", + "odometer_saved": "Bendras ciklų skaičius išsaugotas", + "odometer_save_failed": "Nepavyko išsaugoti bendro skaičiaus: {error}" }, "suggestion": { "both_agree": "WashData rekomenduoja", @@ -1883,10 +1925,6 @@ "higher": "Patvirtina tik aukšto pasitikėjimo atitikimus; daugiau ciklų lieka nepažymėtų", "lower": "Atitinka daugiau ciklų; kai kurie gali būti šiek tiek neteisingi" }, - "running_dead_zone": { - "higher": "Ignoruoja trumpus galios šuolius neveiklumo metu – mažiau jautrus triukšmui", - "lower": "Reaguoja į trumpesnius galios pliūpsnius – aptinka greitą trumpalaikę veiklą" - }, "start_threshold_w": { "higher": "Išvengia netikrų paleidimų dėl trumpų galios šuolių", "lower": "Aptinka mažos galios programas; gali suaktyvėti nuo triukšmo" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} švarių ciklų ({pct}%) turėjo >60s vidinę pauzę, kuri vėl tęsėsi; reikalauti {val} pabaigos patvirtinimo(-ų).", "learning_confidence": "p05 patikimumas {cycles} naudotojo pažymėtų ciklų ({p05}); žemiau šios ribos prašyti patikrinimo.", "match_interval": "Pagrįsta stebimu atnaujinimo dažniu (mediana={median}s) * 10.", + "match_interval_capped": "Apribota taip, kad {persistence} iš eilės einančios atitiktys tilptų į {pct}% trumpiausios programos ({shortest}s); vien atnaujinimo dažnis (mediana={median}s) būtų pasiūlęs ilgesnį intervalą.", + "match_interval_floored": "Vien trumpiausia programa ({shortest}s) apribotų šią reikšmę iki {cap}s, o tai mažiau nei {minimum}s minimumas, todėl paliekama ties minimumu: {persistence} iš eilės einančios atitiktys užtrunka {budget}s, t. y. daugiau nei {pct}% tos programos. Atitiktis skaičiuojama tik gavus naują rodmenį (mediana={median}s), todėl minimumas nieko nekainuoja.", "max_duration_ratio": "Pagrįsta pažymėtų ciklų trukmėmis (p95={p95}).", "min_active": "Pagrįsta mažiausia aktyvia galia ({min}W), stebėta paskutiniame cikle.", "min_duration_ratio": "Paliktas kuo mažesnis, kad programa būtų atpažinta ciklo pradžioje; patikimumo ir dviprasmiškumo slenksčiai neleidžia patvirtinti per anksti.", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Indaplovė: tylos sekundės po numatytos trukmės, kol atleidžiamas ciklo pabaigos vandens išleidimo laukimas", "smart_termination_duration_ratio": "Atitiktos programos numatytos trukmės dalis, kurią ciklas turi pasiekti, kad išmanusis užbaigimas galėtų jį užbaigti anksčiau; sumažinkite ją nuo apkrovos ar temperatūros priklausančioms mašinoms", + "anti_crease_finalize_ratio": "Atitiktos programos numatytos trukmės dalis, kurią ciklas turi pasiekti, kad apsaugos nuo raukšlių sukimo uodegą būtų galima užbaigti; sumažinkite ją džiovyklėse su ilga sukimo uodega, skalbyklėms palikite numatytąją vertę", "anti_wrinkle_enabled": "Sugerkite būgninius impulsus po pagrindinės fazės, o ne skaitykite juos kaip naujus ciklus", "anti_wrinkle_exit_power": "Kad apsauga nuo raukšlių išliktų aktyvi, galia tarp impulsų turi nukristi žemiau šios ribos", "anti_wrinkle_idle_timeout": "Tylos laikas, leidžiamas tarp dviejų būgninių impulsų prieš pasibaigiant apsaugai nuo raukšlių", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "3 etapas: DDTW pusiau prisotinimo atstumas; mažesnis = jautresnis formos skirtumams (numatytoji 30)", "dtw_refine_top_n": "3 etapas: kandidatai, kuriems DTW perskaičiuoja balus; padidinkite iki 7-9 jei teisingas profilis patenka į 4-5 vietą (numatytoji 5)", "duration_scale": "4 etapas: logaritmas, ties kuriuo trukmės atitikimas perpus sumažėja; mažesnis = griežtesnė bausmė (numatytoji 0,175)", - "energy_scale": "4 etapas: logaritmas, ties kuriuo energijos atitikimas perpus sumažėja; mažesnis = griežtesnė bausmė (numatytoji 0,25)" + "energy_scale": "4 etapas: logaritmas, ties kuriuo energijos atitikimas perpus sumažėja; mažesnis = griežtesnė bausmė (numatytoji 0,25)", + "curve_preroll_seconds": "Sekundės rodmenų iš nutrauktų paleidimo bandymų, perkeltų į ciklo kreivės pradžią; 0 = išjungta" }, "col": { "profile_tip": "Atitikusios programos pavadinimas. Nepažymėta reiškia, kad ciklo pabaigoje neatitiko joks profilis.", @@ -1998,10 +2040,11 @@ "date_tip": "Data ir laikas, kada ciklas prasidėjo.", "duration_tip": "Bendra ciklo veikimo trukmė nuo pradžios iki pabaigos.", "energy_tip": "Bendras suvartotos energijos kiekis (kWh). Apskaičiuojamas integruojant galią laike.", - "cost_tip": "Šio ciklo energijos kaina, užfiksuota jam pasibaigus pagal tuo metu galiojusią kainą (energija x kaina už kWh). Nustatykite kainą Nustatymuose, kad ji būtų užpildyta.", + "cost_tip": "Šio ciklo energijos kaina, užfiksuota jam pasibaigus. Kai įjungta „Pagal laiką svertinė kaina“, galios kreivė įkainojama pagal kiekvieną akimirką galiojusią kainą; priešingu atveju energija x viena kaina, galiojusi pabaigoje. Nustatykite kainą Nustatymuose, kad ji būtų užpildyta.", "confidence_tip": "Profilio atitikties pasitikėjimas (0–100 %). Kaip glaudžiai ciklo galios kreivė atitiko atpažintą programą.", "health_tip": "ML ciklo būklė (daugiau = geriau). Spustelėkite ciklą, kad jį patikrintumėte ir peržiūrėtumėte.", - "flags_tip": "Ciklo peržiūros, anomalijų ir šaltinio žymės. Užveskite pelę ant piktogramos, kad matytumėte detales." + "flags_tip": "Ciklo peržiūros, anomalijų ir šaltinio žymės. Užveskite pelę ant piktogramos, kad matytumėte detales.", + "cost_dynamic_tip": "Pagal laiką sverta: įkainota pagal ciklo metu galiojusią kainą (faktinė {price} {cur}/kWh)." }, "stat": { "avg": "vid.", @@ -2028,9 +2071,9 @@ "finished": "Ciklas pasiekė galutinę būseną ir baigėsi." }, "store": { + "search_ph2": "Ieškoti pagal prekės ženklą ar modelį…", "your_model_tip": "Tai prietaisas, kurį nurodėte nustatymuose", "your_model": "Jūsų modelis", - "search_brand_ph": "Ieškoti pagal prekės ženklą…", "programs_count": "Programos: {n}", "browse": "Naršyti", "device": "Prietaisas", @@ -2075,7 +2118,8 @@ "golden": "Pakartotinis apdorojimas: atskaitos ciklų žymėjimas", "suggestions": "Pakartotinis apdorojimas: pasiūlymai", "ml_training": "Pakartotinis apdorojimas: ML mokymas", - "health": "Pakartotinis apdorojimas: ciklų sveikata" + "health": "Pakartotinis apdorojimas: ciklų sveikata", + "costs": "Pakartotinis apdorojimas: energijos išlaidos" }, "pg_sweep": { "optimize": "Optimizavimas: {param}" diff --git a/custom_components/ha_washdata/translations/panel/lv.json b/custom_components/ha_washdata/translations/panel/lv.json index 30e9b8aa..829e076e 100644 --- a/custom_components/ha_washdata/translations/panel/lv.json +++ b/custom_components/ha_washdata/translations/panel/lv.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Beigu daļā šī programma uz aptuveni {mins} min apklust, pēc tam pirms pabeigšanas {secs} s patērē aptuveni {watts} W. Novērots {seen} no {measured} izmērītajiem cikliem: ierīce to nedara katrā reizē.", + "quiet_tail": "klusās beigas ~{mins} min", "artifact_tip": "Konstatētas {n} anomālijas (piemēram, durvis atvērtas cikla vidū) – atveriet, lai tās skatītu diagrammā", "auto": "(noteikts automātiski)", "built_in_tag": "iebūvēts", @@ -77,11 +79,14 @@ "approved": "Apstiprināts", "awaiting": "Gaida apstiprinājumu", "imported_tip": "Importēts no kopienas veikala. Izmanto tikai saskaņošanai, netiek ieskaitīts statistikā.", + "unmatchable": "nevar atpazīt", "not_importable": "šeit nav pieejams", "exists": "jau eksistē", - "backfilled_tip": "Noteikts importētā jaudas vēsturē. Ietekmē tikai programmu saskaņošanu, netiek ieskaitīts statistikā." + "backfilled_tip": "Noteikts importētā jaudas vēsturē. Ietekmē tikai programmu saskaņošanu, netiek ieskaitīts statistikā.", + "armed": "(attiecas uz nākamo ciklu)" }, "btn": { + "reset_zoom": "Atiestatīt tuvinājumu", "set_brand_model": "Iestatīt zīmolu un modeli", "refresh_catalog": "Atsvaidzināt katalogu", "add_device": "+ Pievienot ierīci", @@ -215,6 +220,8 @@ "trim": "Apgriezt", "trim_split": "Apgriezt / sadalīt", "undo": "Atsaukt", + "unlabel_selected": "Noņemt atlasīto marķējumu ({n})", + "unlabel_selected_tip": "Noņem profila marķējumu atlasītajiem cikliem. Tie paliek jūsu vēsturē un tos vēlāk var marķēt no jauna - tie tikai vairs neveido šo profilu.", "use": "Izmantot", "wipe_all": "Noslaukiet visus datus", "delete_selected": "Dzēst atlasītos ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Lietot labāko", "pg_apply_to_settings": "Saglabāt iestatījumos", "pg_apply_to_settings_tip": "Kopēt šeit rediģētās vērtības šīs ierīces aktīvajos iestatījumos", - "search": "Meklēt", "import": "Importēt", "disconnect": "Atvienot", "connect_github": "Savienot GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Nolasīt no Home Assistant", "hist_scan": "Meklēt ciklus", "hist_import_n": "Importēt ciklus ({n})", - "hist_goto_cycles": "Parādīt ciklus" + "hist_goto_cycles": "Parādīt ciklus", + "save_total_cycles": "Saglabāt kopējo skaitu" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Pirms saglabāšanas novērsiet konfliktus", "attn_title": "Iestatījumu konflikti: {n}", + "price_entity": { + "power_sensor": "Šis ir ierīces Jaudas sensors, nevis cena. Cenas entītija aizstāj zemāk norādīto fiksēto cenu, tāpēc cikli tiktu aprēķināti pēc vatiem par kWh - notīriet šo lauku.", + "energy_sensor": "Šis ir ierīces Enerģijas skaitītājs, nevis cena. Cenas entītija aizstāj zemāk norādīto fiksēto cenu, tāpēc cikli tiktu aprēķināti pēc skaitītāja rādījuma par kWh - notīriet šo lauku.", + "device_class": "Šī sensora klase ir {dc}, nevis cena par kWh. Cenas entītija aizstāj zemāk norādīto fiksēto cenu - notīriet šo lauku vai norādiet tarifa sensoru.", + "unit": "Šī sensora mērvienība ir {unit}, kas nav cena par kWh. Cenas entītija aizstāj zemāk norādīto fiksēto cenu - notīriet šo lauku vai norādiet tarifa sensoru." + }, "settings_banner": "Iestatījumu konflikti: {n}. Pārbaudiet iezīmētās sadaļas un labojiet tās pirms saglabāšanas.", "settings_banner_btn": "Doties uz pirmo", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Iestatījumu avots", "phase_catalog": "Fāzes katalogs", "playground": "Izmēģinājumu laukums", - "process_history": "Procesa vēsture", + "process_history": "Apstrādāt vēsturi", "settings_history": "Iestatījumu vēsture", "status": "Statuss", "status_graph": "Statusa grafiks", @@ -360,12 +373,12 @@ "wipe_history": "Noslaukiet vēsturi", "recommendations": "Ieteikumi ({n})", "pg_detection_params": "Noteikšanas iestatījumi", - "pg_timeline": "Notikumu laika josla", "pg_outcome": "Simulācijas rezultāts", "pg_across_cycles": "Pa visiem jūsu cikliem", "community_store": "Kopienas veikals", "online_account": "Kopienas veikals un tiešsaistes funkcijas", - "import_power_history": "Jaudas vēstures importēšana" + "import_power_history": "Jaudas vēstures importēšana", + "service_status": "Apkopes statuss" }, "health": { "fair": "Pieņemama profila kvalitāte", @@ -373,6 +386,10 @@ "poor": "⚠ Slikta profila kvalitāte" }, "lbl": { + "envelope_position_tip": "Pozīcija atbilstošās programmas pašas ierakstītajā līknē, {pct}%. To nosaka, salīdzinot šī cikla formu ar profilu, nevis skaitot laiku, tāpēc tā paliek pareiza, ja cikls ir garāks vai īsāks nekā parasti. Tā atjaunojas, kamēr ierīce ir klusa, tāpēc aktīvas fāzes laikā var atpalikt.", + "envelope_position": "līkne {pct}%", + "aria_playground_chart3": "Interaktīva cikla jaudas diagramma: turiet kursoru virs, lai nolasītu laiku/jaudu, ritiniet vai tuviniet ar diviem pirkstiem, velciet, lai pārvietotu", + "device_unresolved": "Nepieejama ierīce ({id})", "drag_to_resize": "Velciet, lai mainītu izmēru", "pg_anti_wrinkle": "Pretgrumbu režīms", "actions": "Darbības", @@ -508,7 +525,7 @@ "profile_index": "profila numurs →", "profile_name": "Profila nosaukums", "program": "Programma", - "program_tip": "Ignorējiet, kurš profils tiek pielīdzināts pašreizējam ciklam. Automātiskā noteikšana ļauj integrācijai automātiski izvēlēties vislabāko atbilstību. Piespraudiet konkrētu programmu, lai piespiestu atbilstību, kad automātiskā noteikšana ir nepareiza vai zināt, kas darbojas.", + "program_tip": "Ignorējiet, kurš profils tiek pielīdzināts pašreizējam ciklam. Automātiskā noteikšana ļauj integrācijai automātiski izvēlēties vislabāko atbilstību. Piespraudiet konkrētu programmu, lai piespiestu atbilstību, kad automātiskā noteikšana ir nepareiza vai zināt, kas darbojas. Izvēlieties programmu pirms ierīces palaišanas, un tā tiks lietota, tiklīdz sāksies nākamais cikls.", "progress": "Progress", "raw_socket": "Neapstrādāts kontakts", "read_only": "Tikai lasāms", @@ -543,6 +560,7 @@ "stage4": "4. posms – atbilstība", "status": "Statuss", "tail_trim": "Apgriešana no beigām (s)", + "tap_to_dismiss": "pieskarieties diagrammai, lai aizvērtu", "timer_auto_pause": "Automātiska pauze", "timer_min": "min", "timer_msg_placeholder": "Ziņojums (neobligāti, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Atlikušais laiks (modelis)", "pg_time_left_model_tip": "Modeļa novērtētais atlikušais laiks (fāzes novērtētājs + ML sajaukums), nevis statisks atpakaļskaitītājs", "phase": "Fāze", - "pg_mode_simulate": "Simulēt", "pg_mode_history": "Testēt uz vēstures", - "pg_mode_sweep": "Skenēšana", "pg_ev_detected": "Noteikts", "pg_ev_match": "Atbilstība apstiprināta", "pg_ev_match_changed": "Atbilstība mainījās", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Pārrakstīt automātiski noteikto gaidstāves jaudas apakšrobežu (atstājiet tukšu automātiskai).", "pg_stress_toggle": "Pārbaudīt dīkstāves apturēšanu", "pg_stress_toggle_desc": "Simulē ierīci, kas paliek gaidstāves patēriņa līmenī pēc ierakstīšanas beigām - rāda, vai un kad WashData aptur ciklu.", - "pg_sweep_2d": "2D siltuma karte", - "pg_sweep_param_y": "2. parametrs", "pg_obj_match": "Atbilstības precizitāte", "pg_obj_endtiming": "Beigu laika precizitāte", "pg_obj_falseend": "Kļūdaino beigu īpatsvars", "pg_obj_overrun": "Ilguma novirze", "pg_obj_ambiguity": "Neskaidrību īpatsvars", - "pg_best_combo": "labākā kombinācija", "pg_mode_optimize": "Optimizēt", "pg_group_matching": "Programmu saskaņošana", "pg_zoom_hint": "veiciet dubultklikšķi, lai atiestatītu tuvinājumu", @@ -706,7 +719,12 @@ "task_history_import_apply": "Ciklu importēšana", "evidence_real_cycles": "Šīs ierīces izpildītie cikli", "evidence_reference_cycles": "Lejupielādēti no kopienas veikala", - "evidence_backfill_cycles": "Atrasti importētā jaudas vēsturē" + "evidence_backfill_cycles": "Atrasti importētā jaudas vēsturē", + "zoom_hint_touch": "tuviniet ar diviem pirkstiem", + "zoom_hint_touch_zoomed": "tuviniet ar diviem pirkstiem · divreiz pieskarieties, lai atiestatītu", + "cycles_since_service": "{since} / {total} cikli", + "total_cycles_run": "cikli kopā", + "correct_total_cycles": "Labot kopējo skaitu" }, "log": { "all_levels": "Visi līmeņi", @@ -756,7 +774,7 @@ "merge_cycles": "Apvienot {n} ciklus", "new_group": "Jauna profilu grupa", "new_phase": "Jauns posms", - "process_history_msg": "Atkārtoti palaist atbilstību, atjaunināt ieteikumus, pārtrenēt ML (ja iespējots) un pārrēķināt ciklu veselību visos saglabātajos ciklos. Tas var aizņemt kādu laiku.", + "process_history_msg": "Atkārtoti palaist atbilstību, atjaunināt ieteikumus, pārtrenēt ML (ja iespējots), pārrēķināt ciklu izmaksas pēc reģistrētajām enerģijas cenām un pārrēķināt ciklu veselību visos saglabātajos ciklos. Tas var aizņemt kādu laiku.", "process_history_ok": "Apstrādāt", "process_history_title": "Apstrādāt vēsturi", "process_recording": "Procesa ierakstīšana", @@ -789,6 +807,7 @@ "history_import": "Jaudas vēstures importēšana" }, "msg": { + "pg_canvas_empty3": "Izvēlieties ciklu augšā un nospiediet Palaist, lai to simulētu. Pēc tam turiet kursoru virs, lai nolasītu vērtības, ritiniet vai tuviniet ar diviem pirkstiem un velciet, lai pārvietotu.", "tail_trim_hint": "Cik sekundes noņemt no beigām", "store_sibling_hint": "Jūsu precīzajam modelim nekas nav kopīgots? Tuvs tā paša zīmola modelis parasti ir labs sākumpunkts.", "store_declare_appliance": "Norādiet WashData, kura ierīce jums pieder, un šajā cilnē tiks parādītas citu tai kopīgotās programmas un ieraksti. Varat arī augšā ievadīt zīmolu un vienkārši paskatīties apkārt.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Pārsniedza parasto jaudas joslu ~{n}s.", "auto_label_intro": "Piešķiriet profilus nemarķētiem cikliem, kuru atbilstības pārliecība notīra slieksni.", "automations_intro": "WashData aktivizē {start} / {end} notikumus un atklāj entītijas, tāpēc paziņojumus un darbības vislabāk veidot kā parastas Home Assistant automatizācijas. Zemāk redzamas automatizācijas, kas izmanto šo ierīci.", - "cleanup_intro": "Katrs iezīmētais cikls ir pārklāts. Atzīmējiet izņēmumus un izdzēsiet, lai notīrītu profilu.", + "cleanup_intro": "Visi marķētie cikli ir pārklāti viens uz otra. Atzīmējiet novirzes un noņemiet tiem marķējumu (tie paliks vēsturē, bet vairs neveidos šo profilu) vai izdzēsiet tos pavisam.", "clear_debug_hint": "Noņemiet saglabātos atkļūdošanas datus, lai atbrīvotu vietu.", "collecting_data": "Notiek datu vākšana. Vēl nepieciešamie cikli, lai varētu sākt precizēšanu: {need} ({current}/{min}).", "compare_overlay_profiles": "Pārklājuma profili (blāvi)", @@ -901,7 +920,7 @@ "preferences_admin": "Preferences, panelis un piekļuves kontrole", "preferences_adv": "Preferences", "prefs_personal": "Tie attiecas tikai uz jūsu Home Assistant kontu.", - "process_history_hint": "Atkārtoti palaist atbilstību visiem saglabātajiem cikliem, atjaunināt precizēšanas ieteikumus, pārtrenēt ML modeļus (ja iespējots) un pārrēķināt ciklu veselību. Palaidiet to pēc pārskatu partijas.", + "process_history_hint": "Atkārtoti palaist atbilstību visiem saglabātajiem cikliem, atjaunināt precizēšanas ieteikumus, pārtrenēt ML modeļus (ja iespējots), pārrēķināt ciklu izmaksas pēc reģistrētajām enerģijas cenām un pārrēķināt ciklu veselību. Palaidiet to pēc vairāku ciklu pārskatīšanas.", "profile_deleted": "Profils dzēsts", "profile_poor_health_detail": "Šim profilam piešķirtajiem cikliem ir nekonsekventas formas vai zema ticamība. Apsveriet iespēju atjaunot aploksni vai pārskatīt marķētos ciklus.", "profiles_intro": "Noklikšķiniet uz profila, lai skatītu statistiku, fāzes un tīrīšanu. Grupējiet gandrīz identiskas programmas (tāda pati forma/ilgums, atšķirīga temperatūra vai centrifūga), lai tās varētu droši izvēlēties.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' ir zems atbilstības rādītājs - tā nesenie cikli stipri atšķiras vai atpazīti vāji. Pārskatiet tā ciklus vai ierakstiet profilu no jauna, lai atpazīšana un laika aplēses paliktu precīzas.", "advisory_shape_drift": "'{name}' ir ievērojami novirzījies no sākotnējās jaudas līknes. Ierīces darbība laika gaitā, iespējams, ir mainījusies (piemēram, katlakmens, nolietojums). Apsveriet šī profila ierakstīšanu no jauna, izmantojot nesenos ciklus.", "advisory_shape_drift_corr": "'{name}' ir ievērojami novirzījies no sākotnējās jaudas līknes (korelācija {corr}). Ierīces darbība laika gaitā, iespējams, ir mainījusies (piemēram, katlakmens, nolietojums). Apsveriet šī profila ierakstīšanu no jauna, izmantojot nesenos ciklus.", + "advisory_unmatchable": "'{name}' nekad netiks atpazīts: tam nav neviena cikla ar jaudas datiem, tāpēc WashData nav ar ko salīdzināt notiekošo ciklu. Iezīmējiet kādu ciklu ar šo programmu (vai ierakstiet vienu), lai profils atgrieztos saskaņošanā.", + "advisory_unmatchable_no_duration": "'{name}' nekad netiks atpazīts: tam ir cikls ar jaudas datiem, bet bez izmantojama ilguma, tāpēc WashData nav ilguma, pēc kura novērtēt atbilstību. Ierakstiet šo programmu no jauna vai norādiet tās paredzamo ilgumu, lai profils atgrieztos saskaņošanā.", "pg_no_alerts": "Šajā palaišanā problēmas netika konstatētas.", "pg_override_active": "Tiek izmantoti jūsu rediģētie iestatījumi pret pašreizējiem", - "pg_history_intro": "Atkārtoti palaidiet savus nesenos ciklus caur īsto detektoru un saskaņotāju. Vispirms rediģējiet noteikšanas iestatījumu režīmā Simulēt, lai šeit redzētu pirms/pēc salīdzinājumu.", "pg_history_empty": "Nospiediet Palaist, lai atkārtoti palaistu savus ciklus.", "pg_history_summary": "{detected}/{total} noteikti · {correct} pareizi saskaņoti", "pg_sweep_no_metric": "Nav pietiekami daudz datu šī mērķa novērtēšanai.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Skenēšana neizdevās.", "hist_scan_expired": "Šī skenēšana vairs nav pieejama. Lūdzu, skenējiet vēlreiz.", "hist_import_failed": "Importēšana neizdevās.", - "imported_history_readonly": "Noteikts importētā jaudas vēsturē. Tas ietekmē programmu saskaņošanu, bet netiek ieskaitīts jūsu statistikā, un to nevar apgriezt vai sadalīt. Izmantojiet pogu Iezīmēt, lai norādītu programmu." + "imported_history_readonly": "Noteikts importētā jaudas vēsturē. Tas ietekmē programmu saskaņošanu, bet netiek ieskaitīts jūsu statistikā, un to nevar apgriezt vai sadalīt. Izmantojiet pogu Iezīmēt, lai norādītu programmu.", + "odometer_intro": "Šis kopējais skaits tikai palielinās. Cikla ieraksta dzēšana to nemaina, un skaitīšana turpinās arī pēc saglabātās vēstures ierobežojuma, tāpēc apkopes atgādinājumi paliek precīzi.", + "odometer_correct_hint": "Iestatiet to, ja WashData ir reģistrējis ciklu, kas nekad nav noticis, vai ja ierīce jau bija veikusi ciklus pirms WashData uzstādīšanas.", + "no_reminders_set": "Vēl nav iestatīts neviens apkopes atgādinājums.", + "toast_program_armed": "Programma iestatīta nākamajam ciklam: {program}" }, "phase_desc": { "anti_crease": "Laiku pa laikam pēc pabeigšanas veiciet īsus gājienus, lai samazinātu grumbiņas.", @@ -1161,6 +1185,10 @@ "label": "Pasīvās žāvēšanas klusā atbrīvošana", "doc": "Kad cikls pārsniedz paredzēto ilgumu, cik ilgi trauku mazgājamai mašīnai jāpaliek klusai (zem Apstāšanās slieksnis), pirms WashData pārtrauc gaidīt galīgo ūdens izvadīšanu un pabeidz ciklu. Palieliniet to, ja jūsu mašīnai ir ilga klusa žāvēšanas fāze pirms vēlas galīgās ūdens izvadīšanas, kas tiek palaista garām - plašāks logs ļauj apgūtajam ilgumam sekot sezonālajai novirzei (aukstāks ieplūstošais ūdens = garāki cikli), nevis fiksēties pie vecā vidējā. Tas var tikai saīsināt gaidīšanu attiecībā pret iekšējo 30 minūšu beigu smailes ierobežojumu, nekad to nepagarina." }, + "anti_crease_finalize_ratio": { + "doc": "Cik tālu atbilstošās programmas paredzētajā ilgumā ciklam jābūt nonākušam, pirms WashData drīkst secināt, ka klusā griešanās, ko tā redz, ir pretgrumbu aste, un tur pabeigt ciklu. Paredzētais ilgums ir programmas vidējais rādītājs, tāpēc žāvētājā, kura sensora žāvēšanas laiks ir atkarīgs no slodzes, lielākā daļa ciklu nekad nesasniedz 98% no tā, un aste paliek līdz rezerves noildzei - bieži 15 līdz 40 minūtes pēc tam, kad mašīna patiesībā jau apstājusies. Šādā žāvētājā samaziniet šo vērtību (piem., 0,75). Veļas mašīnām atstājiet noklusējumu: veļas mašīna lielāko cikla daļu pavada zem iestatījuma Maks. pretgrumbu jauda, tāpēc šī daļa ir vienīgais, kas atšķir kluso mazgāšanas vidusfāzi no astes, un tās samazināšana var uz pusi saīsināt mazgāšanu. Atstājiet tukšu noklusējuma vērtībai (0,98). Šis ir cits iestatījums nekā Viedas pabeigšanas attiecība, kas vada atsevišķu agrīnas pabeigšanas pārbaudi.", + "label": "Pretgrumbu pabeigšanas attiecība" + }, "anti_wrinkle_enabled": { "doc": "Atpazīstiet īsos mazjaudas veļas impulsus, ko žāvētājs izstaro pēc galvenās karsēšanas fāzes, un turiet tos pievienotus gatavajam ciklam, nevis nolasiet tos kā jaunus ciklus.", "label": "Iespējot pretgrumbu noteikšanu" @@ -1193,6 +1221,10 @@ "doc": "Cikli, kas ir īsāki par šo, tiek izmesti kā spoku cikli (pārbaudes darbi, atverot durvis, lai pievienotu zeķi).", "label": "Min. cikla ilgums" }, + "curve_preroll_seconds": { + "doc": "Cik sekundes nolasījumu no pārtrauktiem palaišanas mēģinājumiem WashData drīkst pārnest uz cikla līknes sākumu. Cikla līkne parasti sākas ar to palaišanas mēģinājumu, kurš beidzot izdodas, tāpēc mašīnā, kas pirms iestāšanās ritmā pamēģina vairākas reizes - programmas izvēle, durvju bloķēšana, pirmā ūdens uzpilde - katrā līknē trūkst pirmo īstās darbības minūšu. Iestatiet šo vērtību nedaudz virs garākā šāda jūsu mašīnas iesildīšanās posma (300 s vairumam ir ar uzviju). 0 to izslēdz. Ņemiet vērā, ka ieslēgšana pārceļ reģistrēto sākumu agrāk, tāpēc cikli, kas reģistrēti pirms un pēc izmaiņas, vienai un tai pašai programmai uzrāda atšķirīgu ilgumu; apgūtie vidējie rādītāji mainīsies, līdz vecākie cikli izkritīs no uzskaites.", + "label": "Līknes ievadsekundes" + }, "delay_confirm_seconds": { "doc": "Strāvai jāpaliek gaidstāves joslā tik ilgi, pirms ierīce tiek uzskatīta par gaidīšanas brīdi, nevis darbošanos.", "label": "Apstiprināšanas sekundes" @@ -1230,7 +1262,7 @@ "label": "Beigu atkārtojumu skaits" }, "energy_price_entity": { - "doc": "Sensors ar pašreizējo elektroenerģijas cenu par kWh (piemēram, dinamisks tarifs). Ir prioritāte pār zemāk norādīto statisko cenu. Katrs cikls iesaldē cenu spēkā, kad tas ir beidzies.", + "doc": "Sensors ar pašreizējo elektroenerģijas cenu par kWh (piemēram, dinamisks tarifs). Tai jābūt cenai, nevis pašas kontaktligzdas jaudas vai enerģijas entītijai: tam ir prioritāte pār zemāk norādīto statisko cenu, tāpēc šeit norādīts kWh skaitītājs katru ciklu aprēķina pēc skaitītāja rādījuma, nevis pēc jūsu tarifa. Ja ieslēgtas Laikā svērtās izmaksas, katrs cikls tiek rēķināts pēc cenas, kas bija spēkā katrā tā darbības brīdī; pretējā gadījumā tam tiek fiksēta cena, kas bija spēkā tā beigās.", "label": "Enerģijas cenas entītija" }, "energy_price_static": { @@ -1258,7 +1290,7 @@ "label": "Invertēt ārējo aktivatoru (aktivēt pie IZSLĒGTS)" }, "learning_confidence": { - "doc": "Ja atbilstības rezultāts ir starp šo un Auto-Label Confidence, atsauksmes paziņojumā tiek lūgts pārbaudīt identificēto programmu. Zem šī rezultāta spēle ir pārāk nenoteikta, lai to varētu atklāt. Jāsaglabā zem Auto-Label Confidence.", + "doc": "Ja atbilstības rezultāts ir starp šo un Automātiskās marķēšanas pārliecību, atsauksmes paziņojumā tiek lūgts pārbaudīt identificēto programmu. Zem šī rezultāta atbilstība ir pārāk nenoteikta, lai to rādītu. Tai jāpaliek zem Automātiskās marķēšanas pārliecības.", "label": "Mācīšanās pārliecība" }, "linked_device": { @@ -1326,7 +1358,7 @@ "label": "Aktivēt HA notikumus paziņojumiem" }, "notify_icon": { - "doc": "Izvēles mdi ikona paziņojumam (piem., mdi:washing-machine). Platformas noklusējuma iestatījumam atstājiet tukšu.", + "doc": "Maza ikona, kas tiek rādīta paziņojumā (piem., mdi:washing-machine). Android to zīmē statusa joslā; iOS to rāda lietotnes ikonas vietā, un tam ir nepieciešama pavadošā lietotne 2026.8 vai jaunāka. Atstājiet tukšu, lai izmantotu platformas noklusējuma ikonu.", "label": "Paziņojuma ikona" }, "notify_live_chronometer": { @@ -1426,7 +1458,7 @@ "label": "Atbilstības slieksnis" }, "profile_unmatch_threshold": { - "doc": "Ja tiešraides cikla vidus spēles rezultāts ir zemāks par šo rezultātu, provizoriskā identifikācija tiek dzēsta. Turiet to nedaudz zem atbilstības sliekšņa, lai īss līdzības kritums nemainītu displeju atpakaļ uz nepārspējamu.", + "doc": "Ja tiešraides atbilstības rezultāts cikla vidū noslīd zem šī rezultāta, provizoriskā identifikācija tiek dzēsta. Turiet to nedaudz zem Atbilstības sliekšņa, lai īss līdzības kritums nepārslēgtu attēlojumu atpakaļ uz stāvokli bez atbilstības.", "label": "Neatbilstības slieksnis" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Sekundes sūknis var darboties nepārtraukti, pirms tas tiek atzīmēts kā iespējams iestrēdzis (aktivizē sūkņa iestrēgšanas notikumu).", "label": "Iestrēgušā sūkņa ilgums" }, - "running_dead_zone": { - "doc": "Pēc cikla sākuma strāvas kritumi šajā logā tiek ignorēti. Veļas mazgājamās mašīnas piepilda ar aukstu ūdeni (pirms karsēšanas samazinās gandrīz 0 W) – bez šīs aizsardzības uzpildes fāze izskatās kā cikla beigas. Dati netiek izlaisti: pilna jaudas izsekošana tiek reģistrēta no T=0. Ieteikumu programma mēra jūsu mašīnas faktisko palaišanas modeli un automātiski nosaka to izmērus.", - "label": "Darbības mirušā zona" - }, "sampling_interval": { "doc": "Paredzamais laiks starp sensora rādījumiem – tiek izmantots, lai izlīdzinātu logu un sāktu pareizu atlēcienu. Katrs sensora atjauninājums tiek fiksēts neatkarīgi no šīs vērtības; tas tikai kalibrē pakārtotos aprēķinus. Ieteikumu programma mēra jūsu sensora faktisko ritmu no iepriekšējiem cikliem un iestata to automātiski.", "label": "Paraugu ņemšanas intervāls" @@ -1568,13 +1596,17 @@ "label": "Jaudas profila intervāls", "doc": "Spaiņa lielums profilam power_profile sensora atribūtam (vienots vidējo vatu masīvs pa laika nišām, ko izmanto ārējie plānotāji, piemēram, EMHASS un tibber_prices). Mazāki spaini saglabā īsas jaudas pikas asas; lielāki spaini izlīdzina formu. Noklusējums 15 min. Tikai lasīšanas laikā; neietekmē noteikšanu." }, + "notify_live_silent": { + "label": "Klusi tiešraides atjauninājumi", + "doc": "Tikai iOS. Tiešraides progress tiek atsvaidzināts klusi: katrs atjauninājums pienāk bez skaņas un vibrācijas, ar zemāku, akumulatoru taupošu prioritāti, ko iOS var uz brīdi apvienot. Pirmais cikla atjauninājums (tas, kas sāk Live Activity) un sākuma un beigu paziņojumi joprojām paziņo kā parasti. Izslēdziet to, ja vēlaties saņemt paziņojumu par katru atjauninājumu." + }, "notify_live_sticky": { "label": "Saglabāt tiešraides paziņojumu pēc pieskāriena", "doc": "Tikai Android. Padarīt tiešraides progresa paziņojumu pastāvīgu (lipīgu), lai pieskāriens tam neatceltu notiekošo pavedienu. Izslēgts saglabā noklusēto uzvedību, kad pieskāriens paziņojumu noraida." }, "notify_live_click_action": { - "label": "Tiešraides paziņojuma pieskāriena mērķis", - "doc": "Tikai Android. Kur atveras tiešraides progresa paziņojuma pieskāriens (piemēram, /lovelace/laundry vai pilns URL), nevis lietotnes sākumlapa. Atstājiet tukšu noklusējumam." + "label": "Paziņojuma pieskāriena mērķis", + "doc": "Kur atveras pieskāriens šīs ierīces paziņojumam, gan Android, gan iOS. Tukšs atver WashData paneli ar šo ierīci. Ievadiet informācijas paneļa ceļu (piem., /lovelace/laundry) vai pilnu URL, lai novirzītu citur, vai “none”, lai pieskārienam nebūtu nekāda mērķa." }, "door_opens_at_end": { "label": "Durvis automātiski atveras beigās", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Cikli, kas veido programmu", "doc": "Kuri cikli tiek izmantoti katras programmas jaudas līknes izveidei un pabeigta cikla saskaņošanai ar to. Ja kādam veidam noņemat atzīmi, tas vairs neietekmē jūsu programmas, taču nekas netiek dzēsts - cikli paliek sarakstā Cikli, un tos joprojām var iezīmēt vai izņemt. Noderīgi, ja neuzticaties importētajiem datiem. Statistiku tas neietekmē: tajā vienmēr tiek ieskaitīti tikai tie cikli, ko šī ierīce patiešām ir izpildījusi. Visu atzīmju noņemšana tiek ignorēta, jo programma bez neviena cikla nekad nevarētu tikt saskaņota." + }, + "energy_price_dynamic": { + "label": "Laikā svērtās izmaksas", + "doc": "Katra cikla izmaksas tiek rēķinātas pēc cenas, kas bija spēkā katrā tā darbības brīdī, nevis pēc vienas cenas, kas bija spēkā tā beigās. Darbojas tikai ar cenas entītiju (statiska cena nemainās). Papildu iestatīšana nav vajadzīga: cikla laikā cena tiek izsekota reāllaikā, bet sadaļā Diagnostika pieejamā funkcija Apstrādāt vēsturi pārrēķina vecāku ciklu izmaksas no recorder datiem. Izslēgts = klasiskā darbība, viena cena, kas fiksēta beigās." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Sadala…", "deleting": "Dzēš…", "imported": "Importēts", - "preparing": "Sagatavo…" + "preparing": "Sagatavo…", + "unlabelling": "Noņem marķējumu…" }, "tab": { "advanced": "Papildu", @@ -1778,6 +1815,7 @@ "recording_stopped": "Ierakstīšana pārtraukta", "review_saved": "Atsauksme saglabāta", "settings_conflicts": "Novērsiet izceltās iestatījumu pretrunas pirms saglabāšanas.", + "settings_no_changes": "Nav izmaiņu, ko saglabāt", "settings_reverted": "Iestatījumi atjaunoti; integrācijas pārlādēšana", "settings_saved": "Iestatījumi saglabāti; integrācijas pārlādēšana", "suggestions_applied": "Ieteikumi piemēroti; integrācijas pārlādēšana", @@ -1836,7 +1874,11 @@ "export_selective_done": "Eksports lejupielādēts", "import_selective_done": "Importēti {profiles} profili un {cycles} cikli", "hist_csv_required": "Vispirms ielādējiet CSV failu vai ielīmējiet tā saturu", - "file_read_failed": "Šo failu nevarēja nolasīt" + "file_read_failed": "Šo failu nevarēja nolasīt", + "unlabel_done": "Marķējums noņemts cikliem: {count}", + "unlabel_failed": "Marķējuma noņemšana neizdevās: {error}", + "odometer_saved": "Kopējais ciklu skaits saglabāts", + "odometer_save_failed": "Neizdevās saglabāt kopējo skaitu: {error}" }, "suggestion": { "both_agree": "WashData iesaka", @@ -1883,10 +1925,6 @@ "higher": "Apstiprina tikai augstas pārliecības atbilstības; vairāk ciklu paliek bez iezīmes", "lower": "Atbilst vairākiem cikliem; daži var būt nedaudz nepareizi" }, - "running_dead_zone": { - "higher": "Ignorē īsus jaudas pieauguma momentus dīkstāvē – mazāk jutīgs pret troksni", - "lower": "Reaģē uz īsākiem jaudas uzliesmojumiem – uztver ātru pārejošu aktivitāti" - }, "start_threshold_w": { "higher": "Novērš viltus palaišanas no īslaicīgiem jaudas pieaugumiem", "lower": "Uztver mazjaudas programmas; var reaģēt uz troksni" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} tīro ciklu ({pct}%) bija >60s iekšēja pauze, kas atsākās; pieprasīt {val} beigu apstiprinājumu(s).", "learning_confidence": "p05 ticamība {cycles} lietotāja marķētajiem cikliem ({p05}); zem šīs vērtības pieprasīt pārbaudi.", "match_interval": "Pamatots ar novēroto atjaunināšanas biežumu (mediāna={median}s) * 10.", + "match_interval_capped": "Ierobežots tā, lai {persistence} secīgas atbilstības ietilptu {pct}% no īsākās programmas ({shortest}s); tikai atjaunināšanas biežums (mediāna={median}s) būtu ieteicis garāku intervālu.", + "match_interval_floored": "Tikai īsākā programma ({shortest}s) ierobežotu šo vērtību līdz {cap}s, kas ir zem {minimum}s minimuma, tāpēc tā tiek turēta minimumā: {persistence} secīgas atbilstības aizņem {budget}s, kas ir vairāk nekā {pct}% no šīs programmas. Saskaņošana notiek tikai tad, kad pienāk jauns rādījums (mediāna={median}s), tāpēc minimums neko nemaksā.", "max_duration_ratio": "Pamatots ar marķēto ciklu ilgumiem (p95={p95}).", "min_active": "Pamatots ar minimālo aktīvo jaudu ({min}W), kas novērota pēdējā ciklā.", "min_duration_ratio": "Saglabāts pēc iespējas zemāks, lai programma tiktu atpazīta cikla sākumā; ticamības un neskaidrības sliekšņi novērš pāragru apstiprināšanu.", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Trauku mazgājamā mašīna: klusās sekundes pēc paredzētā ilguma, pirms tiek atbrīvota cikla beigu ūdens izvadīšanas gaidīšana", "smart_termination_duration_ratio": "Atbilstošās programmas paredzētā ilguma daļa, kas ciklam jāsasniedz, pirms vieda pabeigšana drīkst to pabeigt agrāk; samaziniet to no slodzes vai temperatūras atkarīgām mašīnām", + "anti_crease_finalize_ratio": "Atbilstošās programmas paredzētā ilguma daļa, kas ciklam jāsasniedz, pirms pretgrumbu griešanās asti drīkst pabeigt; samaziniet to žāvētājiem ar garu griešanās asti, veļas mašīnām atstājiet noklusējumu", "anti_wrinkle_enabled": "Absorbējiet griešanās impulsus pēc galvenās fāzes, nevis nolasiet tos kā jaunus ciklus", "anti_wrinkle_exit_power": "Lai pretgrumbu režīms paliktu aktīvs, jaudai starp impulsiem jānokrītas zem šīs vērtības", "anti_wrinkle_idle_timeout": "Atļautais klusuma laiks starp diviem griešanās impulsiem, pirms pretgrumbu režīms beidzas", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "3. posms: DDTW pusiesātinājuma attālums; mazāks = jutīgāks pret formas atšķirībām (noklusējums 30)", "dtw_refine_top_n": "3. posms: kandidāti, kurus DTW pārvērtē; palieliniet līdz 7-9, ja pareizais profils ierindojas 4.-5. vietā (noklusējums 5)", "duration_scale": "4. posms: logaritmiskais koeficients, kur ilguma atbilstība uz pusēm; mazāks = bargāks sods (noklusējums 0,175)", - "energy_scale": "4. posms: logaritmiskais koeficients, kur enerģijas atbilstība uz pusēm; mazāks = bargāks sods (noklusējums 0,25)" + "energy_scale": "4. posms: logaritmiskais koeficients, kur enerģijas atbilstība uz pusēm; mazāks = bargāks sods (noklusējums 0,25)", + "curve_preroll_seconds": "Sekundes nolasījumu no pārtrauktiem palaišanas mēģinājumiem, kas pārnestas uz cikla līknes sākumu; 0 = izslēgts" }, "col": { "profile_tip": "Atbilstošās programmas nosaukums. Bez birkas nozīmē, ka cikla beigās neatbilda neviens profils.", @@ -1998,10 +2040,11 @@ "date_tip": "Datums un laiks, kad cikls sākās.", "duration_tip": "Kopējais cikla darbības laiks no sākuma līdz beigām.", "energy_tip": "Kopējā patērētā enerģija (kWh). Aprēķināta, integrējot jaudu laikā.", - "cost_tip": "Šā cikla enerģijas izmaksas, fiksētas pabeigšanas brīdī, izmantojot tobrīd spēkā esošo cenu (enerģija x cena par kWh). Iestatiet cenu sadaļā Iestatījumi, lai tās aizpildītu.", + "cost_tip": "Šā cikla enerģijas izmaksas, fiksētas pabeigšanas brīdī. Ja ieslēgtas Laikā svērtās izmaksas, jaudas līkne tiek rēķināta pēc katrā brīdī spēkā esošās cenas; pretējā gadījumā enerģija x viena cena, kas bija spēkā beigās. Iestatiet cenu sadaļā Iestatījumi, lai tās aizpildītu.", "confidence_tip": "Profila atbilstības pārliecība (0-100%). Cik cieši cikla jaudas līkne atbilda identificētajai programmai.", "health_tip": "ML cikla veselība (augstāks = labāk). Noklikšķiniet uz cikla, lai to apskatītu un pārskatītu.", - "flags_tip": "Cikla pārskatīšanas, anomāliju un avota karodziņi. Pārvietojiet peli virs ikonas, lai skatītu detaļas." + "flags_tip": "Cikla pārskatīšanas, anomāliju un avota karodziņi. Pārvietojiet peli virs ikonas, lai skatītu detaļas.", + "cost_dynamic_tip": "Laikā svērts: rēķināts pēc cikla laikā spēkā esošās cenas (faktiskā {price} {cur}/kWh)." }, "stat": { "avg": "vidēji", @@ -2028,9 +2071,9 @@ "finished": "Cikls sasniedza gala stāvokli un beidzās." }, "store": { + "search_ph2": "Meklēt pēc zīmola vai modeļa…", "your_model_tip": "Šī ir ierīce, ko norādījāt iestatījumos", "your_model": "Jūsu modelis", - "search_brand_ph": "Meklēt pēc zīmola…", "programs_count": "Programmas: {n}", "browse": "Pārlūkot", "device": "Ierīce", @@ -2075,7 +2118,8 @@ "golden": "Atkārtota apstrāde: atskaites ciklu atzīmēšana", "suggestions": "Atkārtota apstrāde: ieteikumi", "ml_training": "Atkārtota apstrāde: ML apmācība", - "health": "Atkārtota apstrāde: ciklu veselība" + "health": "Atkārtota apstrāde: ciklu veselība", + "costs": "Atkārtota apstrāde: enerģijas izmaksas" }, "pg_sweep": { "optimize": "Optimizēšana: {param}" diff --git a/custom_components/ha_washdata/translations/panel/mk.json b/custom_components/ha_washdata/translations/panel/mk.json index 23d9c077..98ee972c 100644 --- a/custom_components/ha_washdata/translations/panel/mk.json +++ b/custom_components/ha_washdata/translations/panel/mk.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Кон крајот оваа програма стивнува околу {mins} мин, потоа повлекува околу {watts} Вт во тек на {secs} сек и завршува. Забележано кај {seen}/{measured} измерени циклуси: уредот не го прави тоа при секое пуштање.", + "quiet_tail": "~{mins} мин тишина на крајот", "artifact_tip": "Откриени се {n} аномалии (на пр. вратата отворена во средината на циклусот) - отворени за да ги видите на графиконот", "auto": "(автоматски откриено)", "built_in_tag": "вграден", @@ -77,11 +79,14 @@ "approved": "Одобрено", "awaiting": "Чека одобрување", "imported_tip": "Увезено од продавницата на заедницата. Се користи само за совпаѓање, не се брои во статистиката.", + "unmatchable": "не може да се совпадне", "not_importable": "неприменливо", "exists": "постои", - "backfilled_tip": "Откриено во увезена историја на моќност. Влијае само на совпаѓањето на програмите, не се брои во статистиката." + "backfilled_tip": "Откриено во увезена историја на моќност. Влијае само на совпаѓањето на програмите, не се брои во статистиката.", + "armed": "(важи за следниот циклус)" }, "btn": { + "reset_zoom": "Ресетирај зум", "set_brand_model": "Постави бренд и модел", "refresh_catalog": "Освежи го каталогот", "add_device": "+ Додај уред", @@ -160,7 +165,7 @@ "pg_autofill": "Автоматско пополнување", "play": "Пушти", "process": "Процес", - "process_history": "Процесирајте сега", + "process_history": "Обработи сега", "process_recording": "Зачувај снимање", "process_tip": "Зачувај го снимениот траг како нов или постоечки профил", "rebuild": "Обновете ги пликовите", @@ -217,6 +222,8 @@ "trim": "Скрати", "trim_split": "Скрати / Раздели", "undo": "Врати", + "unlabel_selected": "Отстрани ја етикетата од избраните ({n})", + "unlabel_selected_tip": "Ја отстранува етикетата на профилот од избраните циклуси. Тие остануваат во вашата историја и може повторно да се означат подоцна - само престануваат да го обликуваат овој профил.", "use": "Користете", "wipe_all": "Избришете ги сите податоци", "delete_selected": "Избриши избрани ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Примени најдобро", "pg_apply_to_settings": "Зачувај во поставките", "pg_apply_to_settings_tip": "Копирај ги тука изменетите вредности во активните поставки на овој уред", - "search": "Пребарај", "import": "Увези", "disconnect": "Прекини врска", "connect_github": "Поврзи GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Прочитај од Home Assistant", "hist_scan": "Побарај циклуси", "hist_import_n": "Увези циклуси ({n})", - "hist_goto_cycles": "Прикажи ги циклусите" + "hist_goto_cycles": "Прикажи ги циклусите", + "save_total_cycles": "Зачувај го вкупниот број" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Поправете ги конфликтите пред зачувување", "attn_title": "Конфликти во поставките: {n}", + "price_entity": { + "power_sensor": "Ова е Сензорот за моќност на уредот, а не цена. Ентитет на цена има предност пред статичната цена подолу, па циклусите би се пресметувале по вати за kWh - исчистете го ова поле.", + "energy_sensor": "Ова е Мерачот на енергија на уредот, а не цена. Ентитет на цена има предност пред статичната цена подолу, па циклусите би се пресметувале по показанието на мерачот за kWh - исчистете го ова поле.", + "device_class": "Класата на овој сензор е {dc}, а не цена по kWh. Ентитет на цена има предност пред статичната цена подолу - исчистете го ова поле или насочете го кон сензор за тарифа.", + "unit": "Овој сензор прикажува вредности во {unit}, што не е цена по kWh. Ентитет на цена има предност пред статичната цена подолу - исчистете го ова поле или насочете го кон сензор за тарифа." + }, "settings_banner": "Конфликти во поставките: {n}. Проверете ги означените раздели и поправете ги пред зачувување.", "settings_banner_btn": "Оди на прв", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Извор на поставките", "phase_catalog": "Фазен каталог", "playground": "Песочник", - "process_history": "Историја на процесот", + "process_history": "Обработка на историјата", "settings_history": "Историја на поставки", "status": "Статус", "status_graph": "График на статус", @@ -360,12 +373,12 @@ "wipe_history": "Избришете ја историјата", "recommendations": "Препораки ({n})", "pg_detection_params": "Поставки за откривање", - "pg_timeline": "Хронологија на настани", "pg_outcome": "Резултат од симулацијата", "pg_across_cycles": "Низ сите ваши циклуси", "community_store": "Продавница на заедницата", "online_account": "Продавница на заедницата и онлајн функции", - "import_power_history": "Увоз на историја на моќност" + "import_power_history": "Увоз на историја на моќност", + "service_status": "Статус на сервисот" }, "health": { "fair": "Прифатлив квалитет на профилот", @@ -373,6 +386,10 @@ "poor": "⚠ Лош квалитет на профилот" }, "lbl": { + "envelope_position_tip": "Позиција на сопствената снимена крива на совпаднатата програма, {pct}%. Се мери со порамнување на овој циклус со профилот наместо со броење време, па останува точна кога пуштањето е подолго или пократко од вообичаеното. Се освежува додека уредот е тивок, па во текот на активна фаза може да доцни.", + "envelope_position": "крива {pct}%", + "aria_playground_chart3": "Интерактивен график на моќноста на циклусот: посочете за да видите време/моќност, лизгајте или користете два прста за зумирање, влечете за поместување", + "device_unresolved": "Недостапен уред ({id})", "drag_to_resize": "Повлечете за промена на големината", "actions": "Акции", "activity": "Активност", @@ -509,7 +526,7 @@ "profile_index": "индекс на профил →", "profile_name": "Име на профилот", "program": "Програма", - "program_tip": "Заменете го профилот кој е поврзан со тековниот циклус. Автоматското откривање му овозможува на интеграцијата автоматски да го избере најдоброто совпаѓање. Закачете одреден програм за принудно совпаѓање кога автоматското откривање е погрешно или знаете што работи.", + "program_tip": "Заменете го профилот кој е поврзан со тековниот циклус. Автоматското откривање му овозможува на интеграцијата автоматски да го избере најдоброто совпаѓање. Закачете одреден програм за принудно совпаѓање кога автоматското откривање е погрешно или знаете што работи. Изберете програма пред да го стартувате апаратот и таа ќе се примени штом ќе започне следниот циклус.", "progress": "Напредок", "raw_socket": "Суров приклучок", "read_only": "Само за читање", @@ -535,7 +552,7 @@ "settings_basic": "Основно", "settings_detail_level": "Ниво на детали на поставките", "settings_overrides": "Заменети поставки", - "show_debug": "Прикажи картичка за отстранување грешки во живо на натпреварот на страницата Статус (доверба, нејасност, најдобри кандидати)", + "show_debug": "Прикажи картичка за отстранување грешки при совпаѓање во живо на страницата Статус (доверба, нејасност, најдобри кандидати)", "show_expected": "Прикажи очекувано преклопување на кривата (совпаднат профил, портокалова)", "show_raw": "Прикажи прекинувач за необработени отчитувања на приклучокот во живиот графикон на моќноста", "sparkline": "Неодамнешен тренд на времетраење на циклусите", @@ -544,6 +561,7 @@ "stage4": "Етапа 4 – согласување", "status": "Статус", "tail_trim": "Скратување на крајот (с)", + "tap_to_dismiss": "допрете го графиконот за да затворите", "timer_auto_pause": "Автоматска пауза", "timer_min": "мин", "timer_msg_placeholder": "Порака (изборно, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Преостанато време (модел)", "pg_time_left_model_tip": "Проценка на преостанатото време од моделот (проценувач на фази + ML), а не статичко одбројување", "phase": "Фаза", - "pg_mode_simulate": "Симулација", "pg_mode_history": "Тест врз историја", - "pg_mode_sweep": "Пребарување", "pg_ev_detected": "Откриено", "pg_ev_match": "Совпаѓањето е прифатено", "pg_ev_match_changed": "Совпаѓањето е променето", @@ -612,14 +628,11 @@ "pg_sweep_param": "Параметар", "pg_objective": "Цел", "pg_steps": "Чекори", - "pg_sweep_2d": "2D топлинска карта", - "pg_sweep_param_y": "2-ри параметар", "pg_obj_match": "Точност на совпаѓања", "pg_obj_endtiming": "Точност на завршување", "pg_obj_falseend": "Стапка на лажни завршувања", "pg_obj_overrun": "Отстапување во траење", "pg_obj_ambiguity": "Стапка на двосмисленост", - "pg_best_combo": "најдобра комбинација", "pg_mode_optimize": "Оптимизирај", "pg_group_matching": "Совпаѓање на програми", "pg_zoom_hint": "двоен клик го ресетира зумот", @@ -706,7 +719,12 @@ "task_history_import_apply": "Увоз на циклуси", "evidence_real_cycles": "Циклуси што ги извршил овој уред", "evidence_reference_cycles": "Преземени од продавницата на заедницата", - "evidence_backfill_cycles": "Најдени во увезена историја на моќност" + "evidence_backfill_cycles": "Најдени во увезена историја на моќност", + "zoom_hint_touch": "зумирајте со два прста", + "zoom_hint_touch_zoomed": "зумирајте со два прста · двоен допир за ресетирање", + "cycles_since_service": "{since} / {total} циклуси", + "total_cycles_run": "циклуси вкупно", + "correct_total_cycles": "Поправи го вкупниот број" }, "log": { "all_levels": "Сите нивоа", @@ -756,7 +774,7 @@ "merge_cycles": "Спојување на {n} циклуси", "new_group": "Нова група на профили", "new_phase": "Нова фаза", - "process_history_msg": "Повторно стартувајте совпаѓање, освежете предлози, обучете ML (ако е овозможено) и препресметајте го здравјето на циклусите за сите зачувани циклуси. Ова може да потрае.", + "process_history_msg": "Повторно стартувајте го совпаѓањето, освежете ги предлозите, повторно обучете ML (ако е овозможено), препресметајте ги трошоците според запишаните цени на енергијата и препресметајте го здравјето на циклусите за сите зачувани циклуси. Ова може да потрае.", "process_history_ok": "Обработи", "process_history_title": "Обработи историја", "process_recording": "Процес на снимање", @@ -789,6 +807,7 @@ "history_import": "Увоз на историја на моќност" }, "msg": { + "pg_canvas_empty3": "Изберете циклус погоре и притиснете Стартувај за да го симулирате. Потоа посочете за да ги видите вредностите, лизгајте или користете два прста за зумирање и влечете за поместување.", "tail_trim_hint": "Отстранете толку секунди од крајот", "store_sibling_hint": "Нема ништо споделено за вашиот точен модел? Близок модел од истиот бренд обично е добра почетна точка.", "store_declare_appliance": "Кажете му на WashData кој уред го поседувате и оваа картичка ќе ги прикаже конфигурациите што другите ги споделиле за него. Можете и да напишете бренд погоре за да го разгледате каталогот.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Надмина го вообичаениот опсег на моќност за ~{n}с.", "auto_label_intro": "Доделете профили на неозначени циклуси чијашто доверба за совпаѓање го брише прагот.", "automations_intro": "WashData активира настани {start} / {end} и изложува ентитети, па известувањата и акциите е најдобро да се градат како нормални автоматизации на Home Assistant. Автоматизациите кои го користат овој уред се прикажани подолу.", - "cleanup_intro": "Секој означен циклус е преклопен. Штиклирајте ги оддалечените и избришете за да го исчистите профилот.", + "cleanup_intro": "Секој означен циклус е преклопен. Штиклирајте ги отстапувањата, потоа отстранете им ја етикетата (остануваат во историјата, но веќе не го обликуваат овој профил) или избришете ги целосно.", "clear_debug_hint": "Отстранете ги зачуваните податоци за отстранување грешки за да ослободите простор.", "collecting_data": "Собирање на податоци. Циклуси што уште се потребни пред да почне финото подесување: {need} ({current}/{min}).", "compare_overlay_profiles": "Преклопени профили (бледо)", @@ -853,7 +872,7 @@ "no_fine_tuned": "Сè уште ништо не е дотерано - WashData ги користи своите вградени модели.", "no_logs": "Сè уште нема записи од дневници во бафер.", "no_maintenance": "Сè уште не е забележано одржување.", - "no_match_yet": "Сè уште нема обид за натпревар - ова се пополнува за време на циклус на трчање.", + "no_match_yet": "Сè уште нема обид за совпаѓање - ова се пополнува додека трае циклус.", "no_other_users": "Не се пронајдени други корисници на Home Assistant.", "no_phases": "Не се дефинирани фази.", "no_phases_assigned": "Не се доделени фази.", @@ -902,7 +921,7 @@ "preferences_adv": "Преференци", "prefs_personal": "Овие се однесуваат само на вашата сметка на „Помошник за дома“.", "font_size_hint": "Направете сè во овој панел поголемо или помало. Се применува на вашата сметка на овој уред.", - "process_history_hint": "Повторно стартувајте совпаѓање на сите зачувани циклуси, освежете предлози за подесување, повторно обучете ML модели (ако се овозможени) и препресметајте го здравјето на циклусите. Стартувајте по серија прегледи.", + "process_history_hint": "Повторно стартувајте го совпаѓањето на сите зачувани циклуси, освежете ги предлозите за подесување, повторно обучете ги ML моделите (ако се овозможени), препресметајте ги трошоците според запишаните цени на енергијата и препресметајте го здравјето на циклусите. Стартувајте го ова по серија прегледи.", "profile_deleted": "Профилот е избришан", "profile_poor_health_detail": "Циклусите доделени на овој профил имаат неконзистентни форми или мала доверба. Размислете за обнова на пликот или прегледување на означените циклуси.", "profiles_intro": "Кликнете на профил за статистика, фази и чистење. Групирајте скоро идентични програми (иста форма/траење, различна температура или центрифугирање) така што сигурно се совпаѓаат помеѓу нив.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "Профилот '{name}' има ниска оценка на совпаѓање - неговите неодамнешни циклуси значително варираат или слабо се препознаваат. Прегледајте ги неговите циклуси или повторно снимете го профилот за да останат точни препознавањето и проценките на времето.", "advisory_shape_drift": "Профилот '{name}' значително отстапи од својата првобитна форма на потрошувачка на моќност. Уредот можеби со текот на времето го променил своето однесување (на пр. бигор, абење). Размислете за повторно снимање на овој профил со неодамнешните циклуси.", "advisory_shape_drift_corr": "Профилот '{name}' значително отстапи од својата првобитна форма на потрошувачка на моќност (корелација {corr}). Уредот можеби со текот на времето го променил своето однесување (на пр. бигор, абење). Размислете за повторно снимање на овој профил со неодамнешните циклуси.", + "advisory_unmatchable": "Профилот '{name}' никогаш не може да се совпадне: зад него не стои ниту еден циклус со податоци за моќност, па WashData нема со што да го спореди тековниот циклус. Обележете циклус од оваа програма (или снимете нов) за да се врати во совпаѓањето.", + "advisory_unmatchable_no_duration": "Профилот '{name}' никогаш не може да се совпадне: има циклус со податоци за моќност, но без употребливо времетраење, па WashData нема референтна должина според која би се измерило совпаѓањето. Снимете ја оваа програма повторно или поставете го нејзиното очекувано времетраење за да се врати во совпаѓањето.", "pg_no_alerts": "Во ова извршување не се откриени проблеми.", "pg_override_active": "Се користат вашите изменети поставки наместо тековните", - "pg_history_intro": "Повторно пуштете ги последните циклуси низ вистинскиот детектор и совпаѓач. Прво изменете поставка за откривање во „Симулација“ за да видите тука споредба пред/по.", "pg_history_empty": "Притиснете „Изврши“ за да ги репродуцирате циклусите.", "pg_history_summary": "{detected}/{total} откриени · {correct} совпаднати правилно", "pg_sweep_no_metric": "Нема доволно податоци за оценка на оваа цел.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Скенирањето не успеа.", "hist_scan_expired": "Тоа скенирање повеќе не е достапно. Скенирајте повторно.", "hist_import_failed": "Увозот не успеа.", - "imported_history_readonly": "Откриено во увезена историја на моќност. Влијае на совпаѓањето на програмите, но не се брои во вашата статистика и не може да се скратува или разделува. Користете „Етикета“ за да ја наведете програмата." + "imported_history_readonly": "Откриено во увезена историја на моќност. Влијае на совпаѓањето на програмите, но не се брои во вашата статистика и не може да се скратува или разделува. Користете „Етикета“ за да ја наведете програмата.", + "odometer_intro": "Овој вкупен број само расте. Бришењето на запис за циклус не го менува и продолжува да брои и над ограничувањето на зачуваната историја, така што потсетниците за сервис остануваат точни.", + "odometer_correct_hint": "Поставете го ова ако WashData сними циклус што никогаш не се случил или ако апаратот веќе извршувал циклуси пред да се инсталира WashData.", + "no_reminders_set": "Сè уште нема поставени потсетници за сервис.", + "toast_program_armed": "Поставена програма за следниот циклус: {program}" }, "phase_desc": { "anti_crease": "Повремени кратки превртувања по завршувањето за да се намалат брчките.", @@ -1161,6 +1185,10 @@ "doc": "Откако циклусот ќе го надмине очекуваното траење, колку долго машината за миење садови мора да остане неактивна (под «Праг за стоп») пред WashData да престане да чека финално испумпување и да го заврши циклусот. Зголемете го ако вашата машина има долга тивка фаза на сушење пред доцно финално испумпување што се пропушта - поширок прозорец овозможува наученото траење да ги следи сезонските промени (постудена влезна вода = подолги циклуси) наместо да се заклучи на старата просечна вредност. Тоа само го скратува чекањето во однос на внатрешниот лимит од 30 минути за чекање на финалниот скок (испумпување на вода), но никогаш не го продолжува.", "label": "Тивко завршување на пасивното сушење" }, + "anti_crease_finalize_ratio": { + "doc": "Колку длабоко во очекуваното траење на совпаднатата програма мора да навлезе циклусот пред WashData да смее да заклучи дека тивкото превртување на барабанот што го гледа е завршната опашка против брчки и да го заврши циклусот таму. Очекуваното траење е просекот на програмата, па кај машина за сушење чие време на работа со сензор за влажност зависи од полнењето, повеќето циклуси никогаш не достигнуваат 98% од тој просек, па опашката останува сè до резервниот тајмаут - често 15 до 40 минути откако машината навистина застанала. Кај таква машина за сушење намалете ја оваа вредност (пр. 0.75). Машините за перење оставете ги на стандардната вредност: машината за перење го поминува најголемиот дел од циклусот под поставката Макс. моќност против брчки, па овој дел е единственото што ја одвојува тивката фаза среде перењето од опашката, а неговото намалување може да го преполови перењето. Оставете празно за стандардната вредност (0.98). Ова е поинаква поставка од Соодносот на паметно завршување, кој управува со посебна проверка за предвремено завршување.", + "label": "Сооднос на завршување против брчки" + }, "anti_wrinkle_enabled": { "doc": "Препознајте ги кратките пулсирања со мала моќност што ги испушта машината за сушење по главната топлинска фаза и држете ги прикачени на завршениот циклус наместо да ги читате како нови циклуси.", "label": "Овозможи откривање против брчки" @@ -1182,7 +1210,7 @@ "label": "Макс. моќност против брчки" }, "auto_label_confidence": { - "doc": "Ако резултатот на натпреварот на крајот на циклусот е на или над ова, програмата автоматски се означува без никакво барање за потврда. Подигнете го за да бара поголема сигурност пред автоматско означување; спуштете го за да се автоматизира повеќе. Работи во врска со Довербата за учење под неа.", + "doc": "Ако резултатот на совпаѓањето на крајот на циклусот е еднаков или поголем од ова, програмата автоматски се означува без никакво барање за потврда. Подигнете го за да бара поголема сигурност пред автоматско означување; спуштете го за да се автоматизира повеќе. Работи заедно со Довербата при учење под неа.", "label": "Доверба за автоматско означување" }, "auto_maintenance": { @@ -1193,6 +1221,10 @@ "doc": "Циклусите пократки од ова се отфрлаат како циклуси на духови (тестни, отворање на вратата за додавање чорап).", "label": "Мин. траење на циклус" }, + "curve_preroll_seconds": { + "doc": "Колку секунди отчитувања од прекинати обиди за стартување смее WashData да пренесе на почетокот на кривата на циклусот. Кривата на циклусот вообичаено започнува со обидот за стартување што конечно успева, па кај машина што се обидува неколку пати пред да се смири - избор на програма, заклучување на вратата, прво полнење со вода - првите минути на вистинска активност недостасуваат на секоја крива. Поставете ја оваа вредност малку над најдолгиот таков залет што го има вашата машина (300 s е за повеќето повеќе од доволно). 0 ја исклучува функцијата. Имајте предвид дека вклучувањето го поместува запишаниот почеток порано, па циклусите запишани пред и по промената носат различни траења за истата програма; научените просеци ќе отстапуваат додека постарите циклуси не истечат.", + "label": "Воведни секунди на кривата" + }, "delay_confirm_seconds": { "doc": "Напојувањето мора да остане толку долго во опсегот на подготвеност пред апаратот да се третира како што чека да стартува наместо да работи.", "label": "Секунди за потврда" @@ -1230,7 +1262,7 @@ "label": "Броење на повторување при крај" }, "energy_price_entity": { - "doc": "Сензор со моменталната цена на електрична енергија по kWh (на пр. динамична тарифа). Има предност пред статичната цена подолу. Секој циклус ја замрзнува цената што е во сила кога ќе заврши.", + "doc": "Сензор со тековната цена на струја по kWh (на пр. динамична тарифа). Мора да биде цена, а не сопствениот ентитет за моќност или енергија на приклучникот: има предност пред статичната цена подолу, па мерач на kWh овде го пресметува секој циклус по показанието на мерачот наместо по вашата тарифа. Кога е вклучен Временски пондериран трошок, секој циклус се наплаќа по цената што важела во секој момент од неговото работење; во спротивно на него се замрзнува цената што важела кога завршил.", "label": "Ентитет на цена на енергија" }, "energy_price_static": { @@ -1242,7 +1274,7 @@ "label": "Ентитет на мерач на енергија" }, "expose_debug_entities": { - "doc": "Објавувајте дополнителни дијагностички HA ентитети (доверба на натпреварот, двосмисленост, внатрешни работи). Исклучено го одржува списокот со ентитети чист за нормална употреба.", + "doc": "Објавувајте дополнителни дијагностички HA ентитети (доверба во совпаѓањето, двосмисленост, внатрешни состојби). Исклучено го одржува списокот со ентитети чист за нормална употреба.", "label": "Изложи ентитети за дебагирање" }, "external_end_trigger": { @@ -1258,7 +1290,7 @@ "label": "Инвертирај надворешен тригер (тригер при ИСКЛУЧУВАЊЕ)" }, "learning_confidence": { - "doc": "If the match score falls between this and Auto-Label Confidence, a feedback notification asks you to verify the identified program. Below this score the match is too uncertain to surface. Мора да се чува под Довербата за автоматска етикета.", + "doc": "Ако резултатот на совпаѓањето е помеѓу оваа вредност и Довербата за автоматско означување, известување за повратни информации бара од вас да ја потврдите препознаената програма. Под овој резултат совпаѓањето е премногу несигурно за да се прикаже. Мора да се чува под Довербата за автоматско означување.", "label": "Доверба при учење" }, "linked_device": { @@ -1326,7 +1358,7 @@ "label": "Активирај HA настани за известувања" }, "notify_icon": { - "doc": "Изборна икона mdi за известувањето (на пр. mdi:машина за перење). Оставете празно за стандардната платформа.", + "doc": "Малата икона прикажана на известувањето (на пр. mdi:washing-machine). Android ја црта во статусната лента; iOS ја прикажува наместо иконата на апликацијата и бара придружна апликација 2026.8 или понова. Оставете празно за стандардната икона на платформата.", "label": "Икона на известување" }, "notify_live_chronometer": { @@ -1402,7 +1434,7 @@ "label": "Праг за исклучување на напојувањето" }, "power_sensor": { - "doc": "Ентитетот на сензорот што известува за електрична енергија во вати за овој апарат (на пр. сензор.моќност за мијалник). Откривањето на целиот циклус се заснова на овој сигнал.", + "doc": "Ентитетот на сензорот што известува за електрична енергија во вати за овој апарат (на пр. sensor.washer_power). Откривањето на целиот циклус се заснова на овој сигнал.", "label": "Сензор за моќност" }, "profile_duration_tolerance": { @@ -1426,7 +1458,7 @@ "label": "Праг за совпаѓање" }, "profile_unmatch_threshold": { - "doc": "Ако натпревар во живо од средината на циклусот падне под овој резултат, пробната идентификација се брише. Чувајте го малку под прагот на совпаѓање за краткото опаѓање во сличноста да не го врати екранот на неспоредлив.", + "doc": "Ако совпаѓањето во живо среде циклусот падне под овој резултат, привремената идентификација се брише. Чувајте го малку под Прагот за совпаѓање за краток пад во сличноста да не го врати приказот назад на „без совпаѓање“.", "label": "Праг за несовпаѓање" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Секунди пумпата може да работи непрекинато пред да биде означена како евентуално заглавена (го активира настанот на заглавената пумпа).", "label": "Времетраење за заглавена пумпа" }, - "running_dead_zone": { - "doc": "Откако ќе започне циклусот, падовите на напојувањето во овој прозорец се игнорираат. Машините за перење се полнат со ладна вода (паѓа близу 0 W пред загревањето) - без оваа заштита, таа фаза на полнење изгледа како крај на циклусот. Ова НЕ ги прескокнува податоците: трагата на целосна моќност е снимена од T=0. Моторот за предлози ја мери вистинската шема за стартување на вашата машина и автоматски ја мери оваа големина.", - "label": "Мртва зона при работење" - }, "sampling_interval": { "doc": "Очекувано време помеѓу отчитувањата на сензорот - се користи за правилна големина на прозорецот за измазнување и правилно започнување на отскокнување. Секое ажурирање на сензорот се доловува без оглед на оваа вредност; ги калибрира само пресметките низводно. Моторот за предлози ја мери вистинската каденца на вашиот сензор од минатите циклуси и го поставува тоа автоматски.", "label": "Интервал на земање примероци" @@ -1568,13 +1596,17 @@ "label": "Интервал на профилот на моќност", "doc": "Големина на сегментот за атрибутот на сензорот power_profile по профил (рамна низа просечни вати по слот, користена од надворешни планери како EMHASS и tibber_prices). Помали сегменти ги задржуваат кратките врвови на моќноста остри; поголемите ја израмнуваат формата. Стандардно 15 мин. Само за читање; не влијае на откривањето." }, + "notify_live_silent": { + "label": "Тивки ажурирања во живо", + "doc": "Само за iOS. Освежувајте го напредокот во живо тивко: секое ажурирање пристигнува без звук и вибрации, со понизок приоритет што штеди батерија и што iOS може накратко да го групира. Првото ажурирање во циклусот (она што ја стартува Live Activity), како и известувањата за почеток и крај, и понатаму се огласуваат како и обично. Исклучете го ова за да добивате сигнал при секое ажурирање." + }, "notify_live_sticky": { "label": "Задржи живото известување при допир", "doc": "Само за Android. Нека известувањето за напредокот во живо биде постојано (прикачено) за да не се отфрли со допир. Исклучено го задржува стандарното однесување каде допирот го отфрлува." }, "notify_live_click_action": { - "label": "Цел при допир на живото известување", - "doc": "Само за Android. Каде се отвора допирот на известувањето за напредок во живо (на пр. /lovelace/laundry или целосен URL) наместо почетната страна на апликацијата. Оставете празно за стандарното однесување." + "label": "Цел при допир на известувањето", + "doc": "Каде се отвора допирот на известување од овој уред, на Android и iOS. Празно го отвора панелот WashData на овој уред. Внесете патека до контролна табла (на пр. /lovelace/laundry) или целосен URL за да го испратите на друго место, или „none“ за воопшто да нема цел при допир." }, "door_opens_at_end": { "label": "Вратата се отвора автоматски на крај", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Циклуси што обликуваат програма", "doc": "Кои циклуси се користат за изградба на кривата на моќност на секоја програма и за совпаѓање на завршен циклус со неа. Ако одзначите некој вид, тој престанува да ги обликува вашите програми, но ништо не се брише - циклусите остануваат во списокот Циклуси и сѐ уште можат да се означат или отстранат. Корисно ако не им верувате на увезените податоци. Статистиката не се менува: во неа секогаш се бројат само циклусите што овој уред навистина ги извршил. Одзначувањето на сè се игнорира, бидејќи програма без ниту еден циклус никогаш не би можела да се совпадне." + }, + "energy_price_dynamic": { + "label": "Временски пондериран трошок", + "doc": "Го наплаќа секој циклус по цената што важела во секој момент од неговото работење, наместо по единствената цена што важела кога завршил. Важи само за ентитет на цена (статичната цена не може да се менува). Не бара дополнително поставување: цената се следи во живо додека циклусот трае, а „Обработка на историјата“ во Дијагностика повторно ги пресметува трошоците на постарите циклуси од recorder. Исклучено = класичното однесување, една цена замрзната на крајот." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Разделување…", "deleting": "Бришење…", "imported": "Увезено", - "preparing": "Подготовка…" + "preparing": "Подготовка…", + "unlabelling": "Отстранување на етикетата…" }, "tab": { "advanced": "Напредно", @@ -1778,6 +1815,7 @@ "recording_stopped": "Снимањето прекина", "review_saved": "Прегледот е зачуван", "settings_conflicts": "Поправете ги означените конфликти во поставките пред зачувување.", + "settings_no_changes": "Нема промени за зачувување", "settings_reverted": "Поставките се вратени; интеграција повторно вчитување", "settings_saved": "Зачувани се поставките; интеграција повторно вчитување", "suggestions_applied": "Применети предлози; интеграција повторно вчитување", @@ -1836,7 +1874,11 @@ "export_selective_done": "Извозот е преземен", "import_selective_done": "Увезени се {profiles} профил(и) и {cycles} циклус(и)", "hist_csv_required": "Најпрво вчитајте CSV датотека или вметнете ја нејзината содржина", - "file_read_failed": "Таа датотека не можеше да се прочита" + "file_read_failed": "Таа датотека не можеше да се прочита", + "unlabel_done": "Отстранета етикета од {count} циклуси", + "unlabel_failed": "Отстранувањето на етикетата не успеа: {error}", + "odometer_saved": "Вкупниот број циклуси е зачуван", + "odometer_save_failed": "Вкупниот број не можеше да се зачува: {error}" }, "suggestion": { "both_agree": "WashData препорачува", @@ -1883,10 +1925,6 @@ "higher": "Потврдува само совпаѓања со висока сигурност; повеќе циклуси остануваат без ознака", "lower": "Поврзува повеќе циклуси; некои може да бидат малку погрешни" }, - "running_dead_zone": { - "higher": "Ги игнорира кратките скокови на моќност во неактивност – помалку чувствителен на шум", - "lower": "Реагира на пократки наплив на моќност – ги фаќа брзите преодни активности" - }, "start_threshold_w": { "higher": "Избегнува лажни стартови од кратковремени скокови на моќност", "lower": "Ги фаќа програмите со мала моќност; може да се активира на шум" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} чисти циклуси ({pct}%) имале внатрешна пауза подолга од 60s што продолжила; барај {val} потврда(и) за крај.", "learning_confidence": "Доверба p05 на {cycles} циклуси означени од корисникот ({p05}); под оваа вредност барај проверка.", "match_interval": "Врз основа на забележаната зачестеност на ажурирање (медијана={median}s) * 10.", + "match_interval_capped": "Ограничено така што {persistence} последователни совпаѓања да се сместат во {pct}% од најкратката програма ({shortest}s); самата зачестеност на ажурирање (медијана={median}s) би предложила подолг интервал.", + "match_interval_floored": "Веќе и самата најкратка програма ({shortest}s) би ја ограничила вредноста на {cap}s, под минимумот од {minimum}s, па затоа таа останува на минимумот: низа од {persistence} последователни совпаѓања трае {budget}s, што е повеќе од {pct}% од таа програма. Совпаѓањето се извршува само кога ќе пристигне ново мерење (медијана={median}s), па минимумот не носи дополнително оптоварување.", "max_duration_ratio": "Врз основа на траењата на означените циклуси (p95={p95}).", "min_active": "Врз основа на минималната работна моќност ({min}W) забележана во последниот циклус.", "min_duration_ratio": "Задржано што е можно пониско за програмата да се препознае рано во циклусот; праговите на доверба и двосмисленост спречуваат прерани потврди.", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Машина за миење садови: секунди неактивност по очекуваното траење пред да се ослободи чекањето за испумпување на крајот од циклусот", "smart_termination_duration_ratio": "Дел од очекуваното траење на совпаднатата програма што циклусот мора да го достигне пред Паметното завршување да може да го заврши предвреме; намалете го за машини што зависат од полнењето или температурата", + "anti_crease_finalize_ratio": "Дел од очекуваното траење на совпаднатата програма што циклусот мора да го достигне пред завршната опашка на превртување против брчки да смее да се финализира; намалете го кај машини за сушење со долга опашка на превртување, машините за перење оставете ги на стандардната вредност", "completion_min_seconds": "Најкратко извршување што се смета за вистински циклус", "end_repeat_count": "Ниски отчитувања по ред пред завршување", "interrupted_min_seconds": "Кратки циклуси означени како прекинати", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Фаза 3: растојание на пол-заситување на DDTW; помало = поосетливо на форма (стандардно 30)", "dtw_refine_top_n": "Фаза 3: кандидати кои DTW повторно ги оценува; зголемете на 7-9 ако правилниот профил заземе 4.-5. место (стандардно 5)", "duration_scale": "Фаза 4: лог-однос каде согласноста на траење се преполовува; помал = построга казна (стандардно 0,175)", - "energy_scale": "Фаза 4: лог-однос каде согласноста на енергија се преполовува; помал = построга казна (стандардно 0,25)" + "energy_scale": "Фаза 4: лог-однос каде согласноста на енергија се преполовува; помал = построга казна (стандардно 0,25)", + "curve_preroll_seconds": "Секунди отчитувања од прекинати обиди за стартување пренесени на почетокот на кривата на циклусот; 0 = исклучено" }, "col": { "profile_tip": "Име на совпаднатата програма. „Неозначено“ значи дека на крајот на циклусот не се совпадна ниту еден профил.", @@ -1998,10 +2040,11 @@ "date_tip": "Датум и време кога започна циклусот.", "duration_tip": "Вкупно време на извршување на циклусот од почеток до крај.", "energy_tip": "Вкупно потрошена енергија (kWh). Се пресметува со интегрирање на моќноста во текот на времето.", - "cost_tip": "Цена на енергијата за овој циклус, замрзната при завршување со тогаш важечката цена (енергија × цена по kWh). Поставете цена во Поставки за да се пополни.", + "cost_tip": "Трошок за енергија за овој циклус, замрзнат при завршување. Кога е вклучен Временски пондериран трошок, кривата на моќноста се наплаќа по цената што важи во секој момент; во спротивно енергија x единствената цена што важи на крајот. Поставете цена во Поставки за да се пополни.", "confidence_tip": "Доверба на совпаѓањето на профилот (0–100%). Колку прецизно кривата на моќноста на циклусот се совпадна со идентификуваната програма.", "health_tip": "ML здравје на циклусот (повисоко = подобро). Кликнете на циклус за да го инспектирате и прегледате.", - "flags_tip": "Ознаки на заедницата и показатели за квалитет на податоците" + "flags_tip": "Ознаки на заедницата и показатели за квалитет на податоците", + "cost_dynamic_tip": "Временски пондерирано: наплатено по цената што важела во текот на циклусот (ефективно {price} {cur}/kWh)." }, "stat": { "avg": "просек", @@ -2028,9 +2071,9 @@ "finished": "Циклусот достигна крајна состојба и заврши." }, "store": { + "search_ph2": "Пребарај по марка или модел…", "your_model_tip": "Ова е уредот што го наведовте во поставките", "your_model": "Ваш", - "search_brand_ph": "Пребарување по бренд…", "programs_count": "Програми: {n}", "browse": "Прелистување", "device": "Уред", @@ -2076,7 +2119,8 @@ "golden": "Повторна обработка: пополнување референтни", "suggestions": "Повторна обработка: предлози", "ml_training": "Повторна обработка: МЛ обука", - "health": "Повторна обработка: здравје на циклусите" + "health": "Повторна обработка: здравје на циклусите", + "costs": "Повторна обработка: трошоци за енергија" }, "pg_sweep": { "optimize": "Оптимизирање: {param}" diff --git a/custom_components/ha_washdata/translations/panel/nb.json b/custom_components/ha_washdata/translations/panel/nb.json index f18ce3b4..e14b7a5c 100644 --- a/custom_components/ha_washdata/translations/panel/nb.json +++ b/custom_components/ha_washdata/translations/panel/nb.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Mot slutten blir dette programmet stille i omtrent {mins} min og trekker deretter rundt {watts} W i {secs} s før det er ferdig. Sett i {seen} av {measured} målte sykluser: apparatet gjør det ikke ved hver kjøring.", + "quiet_tail": "~{mins} min stille sluttfase", "artifact_tip": "{n} avvik oppdaget (f.eks. dør åpnet midt i syklusen) – åpne for å se dem på grafen", "auto": "(automatisk oppdaget)", "built_in_tag": "innebygd", @@ -77,11 +79,14 @@ "approved": "Godkjent", "awaiting": "Venter på godkjenning", "imported_tip": "Importert fra fellesskapsbutikken. Brukes kun til matching, telles ikke med i statistikken.", + "unmatchable": "kan ikke matches", "not_importable": "ikke her", "exists": "finnes", - "backfilled_tip": "Oppdaget i importert effekthistorikk. Påvirker bare programmatching, telles ikke med i statistikken." + "backfilled_tip": "Oppdaget i importert effekthistorikk. Påvirker bare programmatching, telles ikke med i statistikken.", + "armed": "(gjelder neste syklus)" }, "btn": { + "reset_zoom": "Tilbakestill zoom", "set_brand_model": "Angi merke og modell", "refresh_catalog": "Oppdater katalog", "add_device": "+ Legg til enhet", @@ -215,6 +220,8 @@ "trim": "Beskjær", "trim_split": "Beskjær / Splitt", "undo": "Angre", + "unlabel_selected": "Fjern merking ({n})", + "unlabel_selected_tip": "Fjerner profilmerkingen fra de valgte syklusene. De blir liggende i historikken din og kan merkes på nytt senere - de påvirker bare ikke lenger denne profilen.", "use": "Bruk", "wipe_all": "Tørk alle data", "delete_selected": "Slett valgte ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Bruk beste", "pg_apply_to_settings": "Lagre i innstillinger", "pg_apply_to_settings_tip": "Kopier verdiene du redigerte her, til de aktive innstillingene for denne enheten", - "search": "Søk", "import": "Importer", "disconnect": "Koble fra", "connect_github": "Koble til GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Les fra Home Assistant", "hist_scan": "Søk etter sykluser", "hist_import_n": "Importer {n} sykluser", - "hist_goto_cycles": "Vis meg syklusene" + "hist_goto_cycles": "Vis meg syklusene", + "save_total_cycles": "Lagre total" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Må være over av-effektterskelen ({pot} W)", "threshold": "Må være under stoppeterskelen ({stop} W) for å ha effekt" }, + "price_entity": { + "power_sensor": "Dette er apparatets Strømsensor, ikke en pris. En prisenhet går foran den statiske prisen nedenfor, så sykluser ville blitt beregnet til watt per kWh - tøm dette feltet.", + "energy_sensor": "Dette er apparatets Energimåler, ikke en pris. En prisenhet går foran den statiske prisen nedenfor, så sykluser ville blitt beregnet til måleravlesningen per kWh - tøm dette feltet.", + "device_class": "Denne sensoren måler {dc}, ikke en pris per kWh. En prisenhet går foran den statiske prisen nedenfor - tøm dette feltet, eller pek det mot en tariffsensor.", + "unit": "Denne sensoren leser av i {unit}, som ikke er en pris per kWh. En prisenhet går foran den statiske prisen nedenfor - tøm dette feltet, eller pek det mot en tariffsensor." + }, "pump_stuck": { "duration": "Må være kortere enn ingen-oppdatering-tidsavbruddet ({to} s) slik at fastnat-alarmen utløses før vaktbikkjen stopper syklusen", "timeout": "Må overstige pumpe-fastnat-varigheten ({ps} s) slik at alarmen utløses før syklusen tvangstopper" @@ -349,7 +362,7 @@ "pg_settings_source": "Kilde for innstillingene", "phase_catalog": "Fasekatalog", "playground": "Sandkasse", - "process_history": "Prosesshistorie", + "process_history": "Behandle historikk", "settings_history": "Innstillingshistorikk", "status": "Status", "status_graph": "Statusgraf", @@ -360,12 +373,12 @@ "wipe_history": "Tørk historikk", "recommendations": "Anbefalinger ({n})", "pg_detection_params": "Deteksjonsinnstillinger", - "pg_timeline": "Hendelsestidslinje", "pg_outcome": "Simuleringsresultat", "pg_across_cycles": "På tvers av syklusene dine", "community_store": "Fellesskapsbutikk", "online_account": "Fellesskapsbutikk og nettfunksjoner", - "import_power_history": "Importer effekthistorikk" + "import_power_history": "Importer effekthistorikk", + "service_status": "Servicestatus" }, "health": { "fair": "Akseptabel profilkvalitet", @@ -373,6 +386,10 @@ "poor": "⚠ Dårlig profilkvalitet" }, "lbl": { + "envelope_position_tip": "Posisjon på det matchede programmets egen registrerte kurve, {pct} %. Måles ved å justere denne syklusen mot profilen i stedet for å telle tid, så den stemmer også når en kjøring er lengre eller kortere enn vanlig. Den oppdateres mens apparatet er stille, og kan derfor henge etter i en aktiv fase.", + "envelope_position": "kurve {pct} %", + "aria_playground_chart3": "Interaktiv effektgraf for syklusen: hold pekeren over for å lese av tid/effekt, rull eller knip for å zoome, dra for å panorere", + "device_unresolved": "Utilgjengelig enhet ({id})", "drag_to_resize": "Dra for å endre størrelse", "actions": "Handlinger", "activity": "Aktivitet", @@ -513,7 +530,7 @@ "profile_index": "profilindeks →", "profile_name": "Profilnavn", "program": "Program", - "program_tip": "Overstyr hvilken profil som matches til gjeldende syklus. Automatisk deteksjon lar integrasjonen velge den beste matchen automatisk. Fest et bestemt program for å tvinge-matche det når automatisk deteksjon er feil eller du vet hva som kjøres.", + "program_tip": "Overstyr hvilken profil som matches til gjeldende syklus. Automatisk deteksjon lar integrasjonen velge den beste matchen automatisk. Fest et bestemt program for å tvinge-matche det når automatisk deteksjon er feil eller du vet hva som kjøres. Velg ett før du starter apparatet, så brukes det så snart neste syklus begynner.", "progress": "Framgang", "raw_socket": "Rå strøm", "read_only": "Skrivebeskyttet", @@ -548,6 +565,7 @@ "stage4": "Trinn 4 – samsvar", "status": "Status", "tail_trim": "Beskjær slutt (s)", + "tap_to_dismiss": "trykk på grafen for å lukke", "timer_auto_pause": "Auto-pause", "timer_min": "min", "timer_msg_placeholder": "Melding (valgfritt, {device}/{program}/{minutes})", @@ -584,9 +602,7 @@ "pg_time_left_model": "Tid igjen (modell)", "pg_time_left_model_tip": "Modellberegnet gjenværende tid (faseberegner + ML-blanding), ikke en statisk nedtelling", "phase": "Fase", - "pg_mode_simulate": "Simuler", "pg_mode_history": "Test på historikk", - "pg_mode_sweep": "Sveip", "pg_ev_detected": "Oppdaget", "pg_ev_match": "Treff bekreftet", "pg_ev_match_changed": "Treff endret", @@ -616,14 +632,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Mål", "pg_steps": "Trinn", - "pg_sweep_2d": "2D-varmekart", - "pg_sweep_param_y": "2. parameter", "pg_obj_match": "Treffnøyaktighet", "pg_obj_endtiming": "Sluttidsnøyaktighet", "pg_obj_falseend": "Falsk-slutt-rate", "pg_obj_overrun": "Varighetsavvik", "pg_obj_ambiguity": "Tvetydighetsrate", - "pg_best_combo": "beste kombinasjon", "pg_mode_optimize": "Optimaliser", "pg_group_matching": "Programgjenkjenning", "pg_zoom_hint": "dobbeltklikk for å tilbakestille zoom", @@ -706,7 +719,12 @@ "task_history_import_apply": "Importerer sykluser", "evidence_real_cycles": "Sykluser denne maskinen har kjørt", "evidence_reference_cycles": "Lastet ned fra fellesskapsbutikken", - "evidence_backfill_cycles": "Funnet i importert effekthistorikk" + "evidence_backfill_cycles": "Funnet i importert effekthistorikk", + "zoom_hint_touch": "knip for å zoome", + "zoom_hint_touch_zoomed": "knip for å zoome · dobbelttrykk for å tilbakestille", + "cycles_since_service": "{since} / {total} sykluser", + "total_cycles_run": "sykluser totalt", + "correct_total_cycles": "Korriger totalen" }, "log": { "all_levels": "Alle nivåer", @@ -756,7 +774,7 @@ "merge_cycles": "Slå sammen {n} sykluser", "new_group": "Ny profilgruppe", "new_phase": "Ny fase", - "process_history_msg": "Kjør matching på nytt, oppdater forslag, tren ML på nytt (hvis aktivert) og beregn syklusenes tilstand på nytt for alle lagrede sykluser. Dette kan ta litt tid.", + "process_history_msg": "Kjør matching på nytt, oppdater forslag, tren ML på nytt (hvis aktivert), beregn kostnaden for syklusene på nytt ut fra registrerte energipriser, og beregn syklusenes tilstand på nytt for alle lagrede sykluser. Dette kan ta litt tid.", "process_history_ok": "Behandl", "process_history_title": "Behandle historikk", "process_recording": "Behandle opptak", @@ -789,6 +807,7 @@ "history_import": "Importer effekthistorikk" }, "msg": { + "pg_canvas_empty3": "Velg en syklus ovenfor og trykk på Kjør for å simulere den. Hold deretter pekeren over for å lese av verdier, rull eller knip for å zoome, og dra for å panorere.", "tail_trim_hint": "Fjern så mange sekunder fra slutten", "store_sibling_hint": "Er ingenting delt for nøyaktig din modell? En nært beslektet modell fra samme merke er vanligvis et godt utgangspunkt.", "store_declare_appliance": "Fortell WashData hvilket apparat du har, så viser denne fanen oppsettene andre har delt for det. Du kan også skrive et merke ovenfor for å se deg rundt.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Gikk over det vanlige effektbåndet i ~{n}s.", "auto_label_intro": "Tilordne profiler til umerkede sykluser hvis matchkonfidens klarerer terskelen.", "automations_intro": "WashData utløser {start} / {end}-hendelser og gjør entiteter tilgjengelige, slik at varsler og handlinger best bygges som vanlige Home Assistant-automatiseringer. Automatiseringer som bruker denne enheten, vises nedenfor.", - "cleanup_intro": "Hver merket syklus er lagt over. Kryss av for uteliggere og slett for å rydde opp i profilen.", + "cleanup_intro": "Hver merket syklus er lagt over. Kryss av for uteliggere, og fjern deretter merkingen deres (de blir liggende i historikken, men påvirker ikke lenger denne profilen), eller slett dem helt.", "clear_debug_hint": "Fjern lagrede feilsøkingsdata for å frigjøre plass.", "collecting_data": "Samler inn data. Sykluser som mangler før finjustering kan starte: {need} ({current}/{min}).", "compare_overlay_profiles": "Overleggsprofiler (svak)", @@ -901,7 +920,7 @@ "preferences_admin": "Preferanser, panel og tilgangskontroll", "preferences_adv": "Preferanser", "prefs_personal": "Disse gjelder kun for Home Assistant-kontoen din.", - "process_history_hint": "Kjør matching på nytt for alle lagrede sykluser, oppdater justeringsforslag, tren ML-modellene på nytt (hvis aktivert) og beregn syklusenes tilstand på nytt. Kjør dette etter en batch med gjennomganger.", + "process_history_hint": "Kjør matching på nytt for alle lagrede sykluser, oppdater justeringsforslag, tren ML-modellene på nytt (hvis aktivert), beregn kostnaden for syklusene på nytt ut fra de registrerte energiprisene dine, og beregn syklusenes tilstand på nytt. Kjør dette etter en batch med gjennomganger.", "profile_deleted": "Profil slettet", "profile_poor_health_detail": "Sykluser som er tilordnet denne profilen har inkonsekvente former eller lav konfidens. Vurder å gjenoppbygge konvolutten eller gjennomgå merkede sykluser.", "profiles_intro": "Klikk på en profil for statistikk, faser og opprydding. Grupper nesten identiske programmer (samme form/varighet, forskjellig temperatur eller spinn) slik at du kan matche pålitelige valg mellom dem.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' har en lav samsvarsscore - de siste syklusene varierer mye eller samsvarer svakt. Gå gjennom syklusene, eller ta opp profilen på nytt slik at gjenkjenning og tidsestimater forblir nøyaktige.", "advisory_shape_drift": "'{name}' har endret seg betydelig fra sin opprinnelige effektkurve. Apparatet kan ha endret oppførsel over tid (f.eks. kalk, slitasje). Vurder å ta opp denne profilen på nytt med nye sykluser.", "advisory_shape_drift_corr": "'{name}' har endret seg betydelig fra sin opprinnelige effektkurve (korrelasjon {corr}). Apparatet kan ha endret oppførsel over tid (f.eks. kalk, slitasje). Vurder å ta opp denne profilen på nytt med nye sykluser.", + "advisory_unmatchable": "'{name}' kan aldri matches: det finnes ingen syklus med effektdata bak den, så WashData har ingenting å sammenligne en pågående syklus med. Merk en syklus av dette programmet (eller ta opp en) for å få det med i matchingen igjen.", + "advisory_unmatchable_no_duration": "'{name}' kan aldri matches: syklusen bak den har effektdata, men ingen brukbar varighet, så WashData har ingen lengde å måle en match mot. Ta opp dette programmet på nytt, eller angi forventet varighet, for å få det med i matchingen igjen.", "pg_no_alerts": "Ingen problemer oppdaget i denne kjøringen.", "pg_override_active": "Bruker dine redigerte innstillinger mot gjeldende", - "pg_history_intro": "Spill av de siste syklusene dine gjennom den ekte deteksjonen og matchingen. Rediger en deteksjonsinnstilling i Simuler først for å se en før/etter-sammenligning her.", "pg_history_empty": "Trykk Kjør for å spille av syklusene dine på nytt.", "pg_history_summary": "{detected}/{total} oppdaget · {correct} matchet riktig", "pg_sweep_no_metric": "Ikke nok data til å score dette målet.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Søket mislyktes.", "hist_scan_expired": "Det søket er ikke lenger tilgjengelig. Søk på nytt.", "hist_import_failed": "Importen mislyktes.", - "imported_history_readonly": "Oppdaget i importert effekthistorikk. Den påvirker programmatching, men telles ikke med i statistikken din og kan ikke beskjæres eller deles. Merk den for å navngi programmet." + "imported_history_readonly": "Oppdaget i importert effekthistorikk. Den påvirker programmatching, men telles ikke med i statistikken din og kan ikke beskjæres eller deles. Merk den for å navngi programmet.", + "odometer_intro": "Denne totalen kan bare gå oppover. Å slette en syklusoppføring endrer den ikke, og den fortsetter å telle utover grensen for lagret historikk, slik at servicepåminnelsene fortsatt stemmer.", + "odometer_correct_hint": "Angi dette hvis WashData har registrert en kjøring som aldri fant sted, eller hvis apparatet allerede hadde kjørt sykluser før WashData ble installert.", + "no_reminders_set": "Ingen servicepåminnelser er satt opp ennå.", + "toast_program_armed": "Program valgt for neste syklus: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Oppvaskmaskin: stille sekunder etter forventet varighet før ventingen på tømming ved slutten av syklusen frigjøres", @@ -1082,7 +1106,7 @@ "energy_weight": "Trinn 4: hvor sterkt energioverensstemmelse påvirker sluttscoren (standard 0,22)", "profile_match_min_duration_ratio": "Trinn 1: korteste sykel som andel av profilvarighet; standard 0,1 (10%)", "profile_match_max_duration_ratio": "Trinn 1: lengste sykel som andel av profilvarighet; standard 1,5 (150%)", - "keep_min_score": "Trinn 2: laveste poeng for å bli i matchen; standard 0,1 tillater svake kandidater, senere trinn velger best", + "keep_min_score": "Trinn 2: laveste poengsum for å gå videre; standard 0,1 slipper gjennom svake samsvar, senere trinn velger det beste", "dtw_blend": "Trinn 3: 0 = bare kjernepoengscore, 1 = bare DTW-score, 0,5 = lik blanding (standard)", "dtw_ensemble_w": "Trinn 3 (ensemble-modus): vekt på skalert L1 kontra derivativ DTW; standard 0,7 foretrekker nivåbevisst", "dtw_ddtw_scale": "Trinn 3: DDTW-halvmetningsavstand; mindre = mer formfølsom (standard 30)", @@ -1093,7 +1117,9 @@ "anti_wrinkle_max_power": "En puls over dette avslutter anti-rynke og åpner en ny syklus", "anti_wrinkle_enabled": "Fange opp trommelpulsene etter hovedfasen i stedet for å lese dem som nye sykluser", "anti_wrinkle_max_duration": "En puls lengre enn dette avslutter anti-rynke og åpner en ny syklus", - "anti_wrinkle_exit_power": "Effekten må falle under dette mellom pulsene for at anti-rynke skal forbli aktiv" + "anti_wrinkle_exit_power": "Effekten må falle under dette mellom pulsene for at anti-rynke skal forbli aktiv", + "anti_crease_finalize_ratio": "Andel av det gjenkjente programmets forventede varighet som en syklus må nå før anti-rynke-etterløpet kan avsluttes; senk den på tørketromler med langt etterløp, la vaskemaskiner stå på standardverdien", + "curve_preroll_seconds": "Sekunder med målinger fra avbrutte startforsøk som tas med fremst i kurven til en syklus; 0 = av" }, "phase_desc": { "anti_crease": "Enkelte korte fall etter ferdigstillelse for å redusere rynker.", @@ -1191,6 +1217,10 @@ "label": "Frigjøring etter stille tørkefase", "doc": "Når syklusen passerer forventet varighet: hvor lenge oppvaskmaskinen må holde seg stille (under Stoppterskel) før WashData slutter å vente på en siste tømming og avslutter syklusen. Øk den hvis maskinen din har en lang stille tørkefase før en sen siste tømming som blir oversett - et bredere vindu lar den innlærte varigheten følge sesongvariasjoner (kaldere tilførselsvann = lengre sykluser) i stedet for å låse seg til det gamle gjennomsnittet. Den forkorter bare ventetiden i forhold til den interne grensen på 30 minutter for sluttoppen, aldri forlenger den." }, + "anti_crease_finalize_ratio": { + "label": "Forhold for anti-rynke-avslutning", + "doc": "Hvor langt inn i det gjenkjente programmets forventede varighet en syklus må være før WashData kan avgjøre at den lave trommelingen den ser er anti-rynke-etterløpet, og avslutte syklusen der. Den forventede varigheten er programmets gjennomsnitt, så på en tørketrommel der kjøretiden med fuktsensor følger tøymengden, når de fleste kjøringer aldri 98% av den, og etterløpet blir i stedet stående til reserve-tidsavbruddet - ofte 15 til 40 minutter etter at maskinen faktisk stoppet. Senk denne verdien (f.eks. 0,75) på en slik tørketrommel. La vaskemaskiner stå på standardverdien: en vaskemaskin ligger under Maks. anti-rynke effekt det meste av syklusen, så denne andelen er det eneste som skiller en stille fase midt i vasken fra etterløpet, og en senking kan halvere en vask. La feltet stå tomt for standarden (0,98). Dette er en annen innstilling enn Forhold for smart avslutning, som styrer en egen kontroll for tidlig avslutning." + }, "anti_wrinkle_enabled": { "doc": "Gjenkjenn de korte laveffekttrommelpulsene en tørketrommel sender ut etter hovedvarmefasen, og hold dem festet til den ferdige syklusen i stedet for å lese dem som nye sykluser.", "label": "Aktiver anti-rynke-deteksjon" @@ -1212,7 +1242,7 @@ "label": "Maks. anti-rynke effekt" }, "auto_label_confidence": { - "doc": "Hvis kampresultatet ved syklusslutt er på eller over dette, merkes programmet automatisk uten noen bekreftelsesmelding. Hev den for å kreve høyere sikkerhet før automerking; senk den for å automatisere mer. Fungerer sammen med Learning Confidence under den.", + "doc": "Hvis samsvarsscoren ved syklusslutt er på eller over denne, merkes programmet automatisk uten noen bekreftelsesmelding. Hev den for å kreve høyere sikkerhet før automerking; senk den for å automatisere mer. Fungerer sammen med Lærings-konfidensgrad under den.", "label": "Auto-merking konfidensgrad" }, "auto_maintenance": { @@ -1223,6 +1253,10 @@ "doc": "Sykluser som er kortere enn dette, forkastes som spøkelsessykluser (prøvekjøringer, åpne døren for å legge til en sokk).", "label": "Min. syklusvarighet" }, + "curve_preroll_seconds": { + "label": "Kurveopptakt", + "doc": "Hvor mange sekunder med målinger fra avbrutte startforsøk WashData kan ta med fremst i kurven til en syklus. Kurven til en syklus begynner normalt ved startforsøket som til slutt lykkes, så på en maskin som prøver seg noen ganger før den kommer ordentlig i gang - programvalg, dørlås, første vanninntak - mangler de første minuttene med reell aktivitet i hver kurve. Sett dette litt høyere enn den lengste slike opptakten maskinen din har (300 s er rikelig for de fleste). 0 slår det av. Vær oppmerksom på at når det slås på, flyttes det registrerte starttidspunktet tidligere, slik at sykluser fra før og etter endringen bærer ulike varigheter for det samme programmet; de innlærte gjennomsnittene vil drive til de eldre syklusene har falt ut." + }, "delay_confirm_seconds": { "doc": "Strømmen må forbli i standby-båndet så lenge før apparatet blir behandlet som venter på å starte i stedet for å kjøre.", "label": "Bekreftelsessekunder" @@ -1260,7 +1294,7 @@ "label": "Slutt gjentaksantall" }, "energy_price_entity": { - "doc": "Sensor med gjeldende strømpris per kWh (f.eks. en dynamisk tariff). Går foran den statiske prisen nedenfor. Hver syklus fryser den gjeldende prisen når den er ferdig.", + "doc": "Sensor med gjeldende strømpris per kWh (f.eks. en dynamisk tariff). Må være en pris, ikke støpselets egen strøm- eller energienhet: den går foran den statiske prisen nedenfor, så en kWh-teller her beregner hver syklus til måleravlesningen i stedet for til tariffen din. Med Tidsvektet kostnad på beregnes hver syklus til prisen som gjaldt i hvert øyeblikk den kjørte; ellers fryses prisen som gjaldt da den var ferdig, fast på den.", "label": "Energiprisenhet" }, "energy_price_static": { @@ -1288,7 +1322,7 @@ "label": "Inverter ekstern utløser (utløs ved AV)" }, "learning_confidence": { - "doc": "Hvis kampresultatet faller mellom dette og Auto-Label Confidence, ber en tilbakemeldingsmelding deg om å bekrefte det identifiserte programmet. Under denne poengsummen er kampen for usikker til å dukke opp. Må holdes under Auto-Label Confidence.", + "doc": "Hvis samsvarsscoren faller mellom denne og Auto-merking konfidensgrad, ber en tilbakemeldingsmelding deg om å bekrefte det identifiserte programmet. Under denne poengsummen er samsvaret for usikkert til å vises. Må holdes under Auto-merking konfidensgrad.", "label": "Lærings-konfidensgrad" }, "linked_device": { @@ -1356,7 +1390,7 @@ "label": "Aktiver HA-hendelser for varsler" }, "notify_icon": { - "doc": "Valgfritt mdi-ikon for varselet (f.eks. mdi:vaskemaskin). La stå tomt for standard plattform.", + "doc": "Det lille ikonet som vises på varselet (f.eks. mdi:washing-machine). Android tegner det i statuslinjen; iOS viser det i stedet for appikonet og krever companion-appen 2026.8 eller nyere. La stå tomt for plattformens standard.", "label": "Varselikon" }, "notify_live_chronometer": { @@ -1456,7 +1490,7 @@ "label": "Samsvarsterskel" }, "profile_unmatch_threshold": { - "doc": "Hvis en direkte kamp midt i syklusen faller under denne poengsummen, slettes den tentative identifikasjonen. Hold den litt under Match-terskelen, slik at et kort fall i likhet ikke snur skjermen tilbake til uovertruffen.", + "doc": "Hvis et direkte samsvar midt i syklusen faller under denne poengsummen, slettes den tentative identifikasjonen. Hold den litt under Samsvarsterskel, slik at et kort fall i likhet ikke setter visningen tilbake til uten treff.", "label": "Ikke-samsvarsterskel" }, "progress_reset_delay": { @@ -1467,10 +1501,6 @@ "doc": "Sekunder kan en pumpe kjøre kontinuerlig før den blir flagget som mulig fastlåst (utløser hendelsen med fast pumpe).", "label": "Pumpe fastsittingsvarighet" }, - "running_dead_zone": { - "doc": "Etter at en syklus starter, ignoreres strømfall i dette vinduet. Vaskemaskiner fylles med kaldt vann (faller nær 0 W før oppvarming) - uten denne beskyttelsen ser fyllingsfasen ut som en syklusslutt. Dette hopper IKKE over data: trasen med full effekt registreres fra T=0. Forslagsmotoren måler maskinens faktiske oppstartsmønster og dimensjonerer dette automatisk.", - "label": "Kjøredødsone" - }, "sampling_interval": { "doc": "Forventet tid mellom sensoravlesninger - brukes til å dimensjonere utjevningsvinduet og starte avsprettingen riktig. Hver sensoroppdatering registreres uavhengig av denne verdien; den kalibrerer bare nedstrømsberegningene. Forslagsmotoren måler sensorens faktiske tråkkfrekvens fra tidligere sykluser og setter denne automatisk.", "label": "Sampling-intervall" @@ -1598,13 +1628,17 @@ "label": "Effektprofil-intervall", "doc": "Bucket-størrelse for effektprofilsensorens attributt power_profile (det flate arrayet med gjennomsnittlige wattverdier per slot som brukes av eksterne planleggere som EMHASS og tibber_prices). Mindre buckets holder korte effekttopper skarpe; større buckets glatter kurven. Standard 15 min. Kun for visning; påvirker ikke deteksjon." }, + "notify_live_silent": { + "label": "Lydløse direkteoppdateringer", + "doc": "Kun iOS. Oppdater direktefremdriften stille: hver oppdatering kommer uten lyd eller vibrasjon, med en lavere, batterisparende prioritet som iOS kan samle opp en kort stund. Den første oppdateringen i en syklus (den som starter direkteaktiviteten) og start- og sluttvarslene varsler fortsatt som vanlig. Slå av dette for å bli varslet ved hver oppdatering." + }, "notify_live_sticky": { "label": "Behold live-varsel ved trykk", "doc": "Kun Android. Gjør live-fremdriftsvarselet vedvarende (sticky) slik at et trykk ikke avviser det. Av: standardadferd der et trykk avviser varselet." }, "notify_live_click_action": { - "label": "Trykkdestinasjon for live-varsel", - "doc": "Kun Android. Hvor et trykk på live-fremdriftsvarselet åpner (f.eks. /lovelace/laundry eller en full URL) i stedet for appens startside. La stå tomt for standard." + "label": "Trykkdestinasjon for varsel", + "doc": "Hvor et trykk på et varsel fra dette apparatet åpner, på Android og iOS. Tomt åpner WashData-panelet på dette apparatet. Angi en dashbord-sti (f.eks. /lovelace/laundry) eller en full URL for å sende det et annet sted, eller «none» for ingen trykkdestinasjon i det hele tatt." }, "door_opens_at_end": { "label": "Dør åpnes automatisk ved avslutning", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Sykluser som former et program", "doc": "Hvilke sykluser som brukes til å bygge opp effektkurven for hvert program, og til å matche en ferdig syklus mot den. Fjerner du haken for en type, former den ikke lenger programmene dine, uten at noe slettes - syklusene blir liggende i Sykluser-listen din og kan fortsatt merkes eller fjernes. Nyttig hvis du ikke stoler på importerte data. Statistikken påvirkes ikke: den teller alltid bare syklusene denne maskinen faktisk har kjørt. Å fjerne alle hakene blir ignorert, siden et program uten sykluser bak seg aldri kunne matche." + }, + "energy_price_dynamic": { + "label": "Tidsvektet kostnad", + "doc": "Beregn hver syklus til prisen som gjaldt i hvert øyeblikk den kjørte, i stedet for den ene prisen som gjaldt da den var ferdig. Gjelder bare for en prisenhet (en statisk pris kan ikke endre seg). Krever ikke noe ekstra oppsett: prisen følges live mens syklusen kjører, og \"Behandle historikk\" under Diagnostikk beregner kostnaden for eldre sykluser på nytt fra recorderen. Av = den klassiske oppførselen, én pris frosset ved slutten." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Deler opp…", "deleting": "Sletter…", "imported": "Importert", - "preparing": "Forbereder…" + "preparing": "Forbereder…", + "unlabelling": "Fjerner merking…" }, "suggestion": { "both_agree": "WashData anbefaler", @@ -1775,10 +1814,6 @@ "higher": "Bare svært sikre treff bekreftes – flere sykluser forblir umerket", "lower": "Flere sykluser matches – noen kan være litt feil" }, - "running_dead_zone": { - "higher": "Korte effekttopper under tomgang ignoreres – mindre støyfølsom", - "lower": "Reagerer på kortere effektimpulser – fanger raske transienter" - }, "start_threshold_w": { "higher": "Unngår falske starter fra korte effekttopper", "lower": "Fanger lavomsatte programmer – kan trigges av støy" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} rene sykluser ({pct}%) hadde en intern pause på >60s som ble gjenopptatt; krever {val} sluttbekreftelse(r).", "learning_confidence": "p05-konfidens for {cycles} brukermerkede sykluser ({p05}); under dette bes det om verifisering.", "match_interval": "Basert på observert oppdateringskadens (median={median}s) * 10.", + "match_interval_capped": "Begrenset slik at {persistence} påfølgende treff får plass innenfor {pct}% av det korteste programmet ({shortest}s); oppdateringskadensen alene (median={median}s) ville ha foreslått et lengre intervall.", + "match_interval_floored": "Bare det korteste programmet ({shortest}s) ville begrense dette til {cap}s, under minimumsgrensen på {minimum}s, så det holdes der: {persistence} treff på rad tar {budget}s, som er mer enn {pct}% av det programmet. Matching kjører bare når en måling kommer inn (median={median}s), så minimum koster ingenting.", "max_duration_ratio": "Basert på varigheter for merkede sykluser (p95={p95}).", "min_active": "Basert på laveste aktive effekt ({min}W) observert i siste syklus.", "min_duration_ratio": "Holdt så lav som mulig slik at et program gjenkjennes tidlig i syklusen; konfidens- og tvetydighetsgrensene hindrer forhastede beslutninger.", @@ -1921,6 +1958,7 @@ "recording_stopped": "Opptaket stoppet", "review_saved": "Anmeldelsen er lagret", "settings_conflicts": "Rett de fremhevede innstillingskonflikter før lagring.", + "settings_no_changes": "Ingen endringer å lagre", "settings_reverted": "Innstillinger gjenopprettet; integrering omlasting", "settings_saved": "Innstillinger lagret; integrering omlasting", "suggestions_applied": "Forslag brukt; integrering omlasting", @@ -1979,7 +2017,11 @@ "export_selective_done": "Eksport lastet ned", "import_selective_done": "Importerte {profiles} profil(er) og {cycles} syklus(er)", "hist_csv_required": "Last inn en CSV-fil eller lim inn innholdet først", - "file_read_failed": "Kunne ikke lese den filen" + "file_read_failed": "Kunne ikke lese den filen", + "unlabel_done": "Merking fjernet fra {count} syklus(er)", + "unlabel_failed": "Fjerning av merking mislyktes: {error}", + "odometer_saved": "Totalt antall sykluser lagret", + "odometer_save_failed": "Kunne ikke lagre totalen: {error}" }, "trend": { "down": "Trender nedover", @@ -1998,10 +2040,11 @@ "date_tip": "Dato og klokkeslett da syklusen startet.", "duration_tip": "Total kjøretid for syklusen fra start til slutt.", "energy_tip": "Totalt forbrukt energi (kWh). Beregnet ved å integrere effekten over tid.", - "cost_tip": "Energikostnad for denne syklusen, låst ved fullføring med prisen som gjaldt da (energi x pris per kWh). Angi en pris under Innstillinger for å fylle den ut.", + "cost_tip": "Energikostnad for denne syklusen, frosset ved fullføring. Med Tidsvektet kostnad på beregnes effektkurven til prisen som gjaldt i hvert øyeblikk, ellers energi x den ene prisen som gjaldt til slutt. Angi en pris under Innstillinger for å fylle den ut.", "confidence_tip": "Profilmatchkonfidens (0-100 %). Hvor tett syklusens effektkurve matchet det identifiserte programmet.", "health_tip": "ML-syklushelse (høyere = bedre). Klikk på en syklus for å inspisere og gjennomgå den.", - "flags_tip": "Gjennomgangs-, anomali- og kildeflagg for syklusen. Hold musepekeren over et ikon for detaljer." + "flags_tip": "Gjennomgangs-, anomali- og kildeflagg for syklusen. Hold musepekeren over et ikon for detaljer.", + "cost_dynamic_tip": "Tidsvektet: beregnet til prisen som gjaldt under syklusen (effektivt {price} {cur}/kWh)." }, "stat": { "avg": "gj.sn.", @@ -2028,9 +2071,9 @@ "finished": "Syklusen nådde en sluttilstand og stoppet." }, "store": { + "search_ph2": "Søk etter merke eller modell…", "your_model_tip": "Dette er apparatet du har angitt i Innstillinger", "your_model": "Ditt apparat", - "search_brand_ph": "Søk etter merke…", "programs_count": "Programmer: {n}", "browse": "Bla gjennom", "device": "Enhet", @@ -2075,7 +2118,8 @@ "golden": "Behandler på nytt: fyller ut referansesykluser", "suggestions": "Behandler på nytt: forslag", "ml_training": "Behandler på nytt: ML-opplæring", - "health": "Behandler på nytt: syklushelse" + "health": "Behandler på nytt: syklushelse", + "costs": "Behandler på nytt: energikostnader" }, "pg_sweep": { "optimize": "Optimaliser: {param}" diff --git a/custom_components/ha_washdata/translations/panel/nl.json b/custom_components/ha_washdata/translations/panel/nl.json index 4582a9d1..b70f889a 100644 --- a/custom_components/ha_washdata/translations/panel/nl.json +++ b/custom_components/ha_washdata/translations/panel/nl.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Tegen het einde wordt dit programma ongeveer {mins} min stil en trekt daarna nog ongeveer {watts} W gedurende {secs} s voordat het klaar is. Gezien in {seen} van {measured} gemeten cycli: het apparaat doet dit niet bij elke draaibeurt.", + "quiet_tail": "~{mins} min stille fase", "artifact_tip": "{n} afwijkingen gedetecteerd (bijvoorbeeld de deur werd halverwege de cyclus geopend) – open om ze in de grafiek te zien", "auto": "(automatisch gedetecteerd)", "built_in_tag": "ingebouwd", @@ -77,11 +79,14 @@ "approved": "Goedgekeurd", "awaiting": "Wacht op goedkeuring", "imported_tip": "Geïmporteerd uit de communitystore. Alleen gebruikt voor matchen, telt niet mee in statistieken.", + "unmatchable": "niet te matchen", "not_importable": "hier n.v.t.", "exists": "bestaat", - "backfilled_tip": "Gedetecteerd in geïmporteerde vermogensgeschiedenis. Beïnvloedt alleen het matchen van programma's, telt niet mee in statistieken." + "backfilled_tip": "Gedetecteerd in geïmporteerde vermogensgeschiedenis. Beïnvloedt alleen het matchen van programma's, telt niet mee in statistieken.", + "armed": "(geldt voor de volgende cyclus)" }, "btn": { + "reset_zoom": "Zoom resetten", "set_brand_model": "Merk en model instellen", "refresh_catalog": "Catalogus vernieuwen", "add_device": "+ Apparaat toevoegen", @@ -215,6 +220,8 @@ "trim": "Bijsnijden", "trim_split": "Bijsnijden / splitsen", "undo": "Ongedaan maken", + "unlabel_selected": "Label van selectie verwijderen ({n})", + "unlabel_selected_tip": "Verwijdert het profiellabel van de geselecteerde cycli. Ze blijven in je geschiedenis staan en kunnen later opnieuw worden gelabeld - ze beïnvloeden dit profiel alleen niet meer.", "use": "Gebruik", "wipe_all": "Wis alle gegevens", "delete_selected": "Selectie verwijderen ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Beste toepassen", "pg_apply_to_settings": "Opslaan in instellingen", "pg_apply_to_settings_tip": "De hier bewerkte waarden kopiëren naar de actieve instellingen van dit apparaat", - "search": "Zoeken", "import": "Importeren", "disconnect": "Verbinding verbreken", "connect_github": "Verbinden met GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Uit Home Assistant lezen", "hist_scan": "Zoeken naar cycli", "hist_import_n": "{n} cycli importeren", - "hist_goto_cycles": "Toon me de cycli" + "hist_goto_cycles": "Toon me de cycli", + "save_total_cycles": "Totaal opslaan" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Moet boven de uitschakeldrempel ({pot} W) liggen", "threshold": "Moet onder de stopdrempel ({stop} W) liggen om van kracht te zijn" }, + "price_entity": { + "power_sensor": "Dit is de Vermogenssensor van het apparaat, geen prijs. Een prijsentiteit heeft voorrang op de onderstaande statische prijs, dus cycli zouden worden afgerekend tegen watt per kWh - maak dit veld leeg.", + "energy_sensor": "Dit is de Energiemeter van het apparaat, geen prijs. Een prijsentiteit heeft voorrang op de onderstaande statische prijs, dus cycli zouden worden afgerekend tegen de meterstand per kWh - maak dit veld leeg.", + "device_class": "Deze sensor meet {dc}, geen prijs per kWh. Een prijsentiteit heeft voorrang op de onderstaande statische prijs - maak dit veld leeg of kies een tariefsensor.", + "unit": "Deze sensor geeft waarden in {unit}, en dat is geen prijs per kWh. Een prijsentiteit heeft voorrang op de onderstaande statische prijs - maak dit veld leeg of kies een tariefsensor." + }, "pump_stuck": { "duration": "Moet kleiner zijn dan de geen-update-timeout ({to} s) zodat het vastgelopen-alarm afgaat vóór de watchdog de cyclus beëindigt", "timeout": "Moet de vastgelopen-duur van de pomp ({ps} s) overschrijden zodat het alarm afgaat vóór de cyclus geforceerd wordt gestopt" @@ -349,7 +362,7 @@ "pg_settings_source": "Herkomst van de instellingen", "phase_catalog": "Fasecatalogus", "playground": "Speeltuin", - "process_history": "Procesgeschiedenis", + "process_history": "Geschiedenis verwerken", "settings_history": "Instellingengeschiedenis", "status": "Status", "status_graph": "Statusgrafiek", @@ -360,12 +373,12 @@ "wipe_history": "Geschiedenis wissen", "recommendations": "Aanbevelingen ({n})", "pg_detection_params": "Detectie-instellingen", - "pg_timeline": "Gebeurtenistijdlijn", "pg_outcome": "Simulatieresultaat", "pg_across_cycles": "Over al je cycli", "community_store": "Communitystore", "online_account": "Communitystore en onlinefuncties", - "import_power_history": "Vermogensgeschiedenis importeren" + "import_power_history": "Vermogensgeschiedenis importeren", + "service_status": "Onderhoudsstatus" }, "health": { "fair": "Acceptabele profielkwaliteit", @@ -373,6 +386,10 @@ "poor": "⚠ Slechte profielkwaliteit" }, "lbl": { + "envelope_position_tip": "Positie op de eigen opgenomen curve van het overeenkomende programma, {pct}%. Bepaald door deze cyclus uit te lijnen op het profiel in plaats van tijd te tellen, dus hij blijft kloppen als een draaibeurt langer of korter duurt dan gebruikelijk. Hij wordt ververst terwijl het apparaat stil is en kan daardoor achterlopen tijdens een actieve fase.", + "envelope_position": "curve {pct}%", + "aria_playground_chart3": "Interactieve vermogensgrafiek van de cyclus: beweeg de muis eroverheen om tijd/vermogen af te lezen, scroll of knijp om te zoomen, sleep om te verschuiven", + "device_unresolved": "Niet-beschikbaar apparaat ({id})", "drag_to_resize": "Sleep om het formaat te wijzigen", "actions": "Acties", "activity": "Activiteit", @@ -513,7 +530,7 @@ "profile_index": "profielindex →", "profile_name": "Profielnaam", "program": "Programma", - "program_tip": "Overschrijft welk profiel wordt gekoppeld aan de huidige cyclus. Automatische detectie laat de integratie automatisch de beste overeenkomst kiezen. Zet een specifiek programma vast om het te forceren wanneer automatische detectie onjuist is of u weet wat er draait.", + "program_tip": "Overschrijft welk profiel wordt gekoppeld aan de huidige cyclus. Automatische detectie laat de integratie automatisch de beste overeenkomst kiezen. Zet een specifiek programma vast om het te forceren wanneer automatische detectie onjuist is of u weet wat er draait. Kies er een voordat u het apparaat start, dan wordt het toegepast zodra de volgende cyclus begint.", "progress": "Voortgang", "raw_socket": "Ruwe stroom", "read_only": "Alleen-lezen", @@ -548,6 +565,7 @@ "stage4": "Stap 4 – overeenkomst", "status": "Status", "tail_trim": "Einde bijsnijden (s)", + "tap_to_dismiss": "tik op de grafiek om te sluiten", "timer_auto_pause": "Automatische pauze", "timer_min": "min", "timer_msg_placeholder": "Bericht (optioneel, {device}/{program}/{minutes})", @@ -584,9 +602,7 @@ "pg_time_left_model": "Resterende tijd (model)", "pg_time_left_model_tip": "Door het model geschatte resterende tijd (fase-schatter + ML-mix), geen statische aftelling", "phase": "Fase", - "pg_mode_simulate": "Simuleren", "pg_mode_history": "Op geschiedenis testen", - "pg_mode_sweep": "Scan", "pg_ev_detected": "Gedetecteerd", "pg_ev_match": "Match vastgelegd", "pg_ev_match_changed": "Match gewijzigd", @@ -616,14 +632,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Doel", "pg_steps": "Stappen", - "pg_sweep_2d": "2D-heatmap", - "pg_sweep_param_y": "2e parameter", "pg_obj_match": "Matchnauwkeurigheid", "pg_obj_endtiming": "Nauwkeurigheid eindtiming", "pg_obj_falseend": "Vals-einde-percentage", "pg_obj_overrun": "Duurafwijking", "pg_obj_ambiguity": "Dubbelzinnigheidspercentage", - "pg_best_combo": "beste combinatie", "pg_mode_optimize": "Optimaliseren", "pg_group_matching": "Programmaherkenning", "pg_zoom_hint": "dubbelklik om de zoom te resetten", @@ -706,7 +719,12 @@ "task_history_import_apply": "Cycli importeren", "evidence_real_cycles": "Cycli die deze machine heeft uitgevoerd", "evidence_reference_cycles": "Gedownload uit de communitystore", - "evidence_backfill_cycles": "Gevonden in geïmporteerde vermogensgeschiedenis" + "evidence_backfill_cycles": "Gevonden in geïmporteerde vermogensgeschiedenis", + "zoom_hint_touch": "knijp om te zoomen", + "zoom_hint_touch_zoomed": "knijp om te zoomen · dubbeltik om te resetten", + "cycles_since_service": "{since} / {total} cycli", + "total_cycles_run": "cycli in totaal", + "correct_total_cycles": "Totaal corrigeren" }, "log": { "all_levels": "Alle niveaus", @@ -756,7 +774,7 @@ "merge_cycles": "Voeg {n} cycli samen", "new_group": "Nieuwe profielgroep", "new_phase": "Nieuwe Fase", - "process_history_msg": "Voer opnieuw matching uit, vernieuw suggesties, train ML opnieuw (indien ingeschakeld) en herbereken de cyclusgezondheid over alle opgeslagen cycli. Dit kan even duren.", + "process_history_msg": "Voer de matching opnieuw uit, vernieuw de suggesties, train ML opnieuw (indien ingeschakeld), herbereken de kosten van de cycli op basis van geregistreerde energieprijzen en herbereken de cyclusgezondheid voor alle opgeslagen cycli. Dit kan even duren.", "process_history_ok": "Verwerken", "process_history_title": "Geschiedenis verwerken", "process_recording": "Opname verwerken", @@ -789,6 +807,7 @@ "history_import": "Vermogensgeschiedenis importeren" }, "msg": { + "pg_canvas_empty3": "Kies hierboven een cyclus en druk op Uitvoeren om deze te simuleren. Beweeg vervolgens de muis erover om waarden af te lezen, scroll of knijp om te zoomen en sleep om te verschuiven.", "tail_trim_hint": "Dit aantal seconden aan het einde verwijderen", "store_sibling_hint": "Niets gedeeld voor precies uw model? Een nauw verwant model van hetzelfde merk is meestal een goed startpunt.", "store_declare_appliance": "Laat WashData weten welk apparaat u bezit, dan toont dit tabblad de setups die anderen ervoor hebben gedeeld. U kunt hierboven ook een merk invoeren om rond te kijken.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Trad boven de gebruikelijke vermogensband voor ~{n}s.", "auto_label_intro": "Wijs profielen toe aan niet-gelabelde cycli waarvan het matchvertrouwen de drempel overschrijdt.", "automations_intro": "WashData activeert {start} / {end}-gebeurtenissen en stelt entiteiten beschikbaar, waardoor meldingen en acties het best als gewone Home Assistant-automatiseringen worden gebouwd. Automatiseringen die dit apparaat gebruiken, worden hieronder weergegeven.", - "cleanup_intro": "Elke gelabelde cyclus wordt overlay. Vink uitschieters aan en verwijder deze om het profiel op te schonen.", + "cleanup_intro": "Alle gelabelde cycli over elkaar heen weergegeven. Vink uitschieters aan en verwijder dan hun label (ze blijven in de geschiedenis, maar beïnvloeden dit profiel niet meer) of verwijder ze definitief.", "clear_debug_hint": "Verwijder opgeslagen foutopsporingsgegevens om ruimte vrij te maken.", "collecting_data": "Gegevens worden verzameld. Nog benodigde cycli voordat verfijning kan beginnen: {need} ({current}/{min}).", "compare_overlay_profiles": "Overlay-profielen (vaag)", @@ -901,7 +920,7 @@ "preferences_admin": "Voorkeuren, paneel- en toegangscontrole", "preferences_adv": "Voorkeuren", "prefs_personal": "Deze zijn alleen van toepassing op uw Home Assistant-account.", - "process_history_hint": "Voer opnieuw matching uit op alle opgeslagen cycli, vernieuw afstemsugesties, train de ML-modellen opnieuw (indien ingeschakeld) en herbereken de cyclusgezondheid. Uitvoeren na een batch van beoordelingen.", + "process_history_hint": "Voer de matching opnieuw uit op alle opgeslagen cycli, vernieuw de afstemmingssuggesties, train de ML-modellen opnieuw (indien ingeschakeld), herbereken de kosten van de cycli op basis van uw geregistreerde energieprijzen en herbereken de cyclusgezondheid. Uitvoeren na een reeks beoordelingen.", "profile_deleted": "Profiel verwijderd", "profile_poor_health_detail": "Cycli die aan dit profiel zijn toegewezen, hebben inconsistente vormen of een lage betrouwbaarheid. Overweeg om de envelop opnieuw te maken of de gelabelde cycli te bekijken.", "profiles_intro": "Klik op een profiel voor statistieken, fasen en opruimacties. Groepeer vrijwel identieke programma's (dezelfde vorm/duur, andere temperatuur of draairichting), zodat er betrouwbaar een keuze tussen de programma's kan worden gemaakt.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' heeft een lage overeenkomstscore - de recente cycli variëren sterk of komen zwak overeen. Bekijk de cycli of neem het profiel opnieuw op zodat de herkenning en tijdschattingen nauwkeurig blijven.", "advisory_shape_drift": "'{name}' is aanzienlijk afgeweken van de oorspronkelijke vermogenscurve. Het apparaat heeft zijn gedrag mogelijk in de loop van de tijd veranderd (bijv. kalkaanslag, slijtage). Overweeg dit profiel opnieuw op te nemen met recente cycli.", "advisory_shape_drift_corr": "'{name}' is aanzienlijk afgeweken van de oorspronkelijke vermogenscurve (correlatie {corr}). Het apparaat heeft zijn gedrag mogelijk in de loop van de tijd veranderd (bijv. kalkaanslag, slijtage). Overweeg dit profiel opnieuw op te nemen met recente cycli.", + "advisory_unmatchable": "'{name}' kan nooit worden gematcht: er zit geen cyclus met vermogensdata achter, dus WashData heeft niets om een lopende cyclus mee te vergelijken. Label een cyclus van dit programma (of neem er een op) om het weer mee te laten doen aan het matchen.", + "advisory_unmatchable_no_duration": "'{name}' kan nooit worden gematcht: de bijbehorende cyclus heeft wel vermogensdata, maar geen bruikbare duur, dus WashData heeft geen looptijd om een match aan af te meten. Neem dit programma opnieuw op of stel de verwachte duur in om het weer mee te laten doen aan het matchen.", "pg_no_alerts": "Geen problemen gedetecteerd in deze run.", "pg_override_active": "Gebruikt je bewerkte instellingen t.o.v. de huidige", - "pg_history_intro": "Speel je recente cycli opnieuw af via de echte detectie en matching. Bewerk eerst een detectie-instelling in Simuleren om hier een voor/na-vergelijking te zien.", "pg_history_empty": "Druk op Uitvoeren om je cycli opnieuw af te spelen.", "pg_history_summary": "{detected}/{total} gedetecteerd · {correct} correct gematcht", "pg_sweep_no_metric": "Niet genoeg gegevens om dit doel te scoren.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Zoeken mislukt.", "hist_scan_expired": "Die zoekopdracht is niet meer beschikbaar. Zoek opnieuw.", "hist_import_failed": "Importeren mislukt.", - "imported_history_readonly": "Gedetecteerd in geïmporteerde vermogensgeschiedenis. Het beïnvloedt het matchen van programma's, maar telt niet mee in je statistieken en kan niet worden bijgesneden of gesplitst. Label het om het programma te benoemen." + "imported_history_readonly": "Gedetecteerd in geïmporteerde vermogensgeschiedenis. Het beïnvloedt het matchen van programma's, maar telt niet mee in je statistieken en kan niet worden bijgesneden of gesplitst. Label het om het programma te benoemen.", + "odometer_intro": "Dit totaal loopt alleen omhoog. Een cyclusregistratie verwijderen verandert het niet, en het blijft doortellen voorbij de limiet van de opgeslagen geschiedenis, zodat serviceherinneringen juist blijven.", + "odometer_correct_hint": "Stel dit in als WashData een draaibeurt heeft geregistreerd die nooit heeft plaatsgevonden, of als het apparaat al cycli had uitgevoerd voordat WashData werd geïnstalleerd.", + "no_reminders_set": "Er zijn nog geen serviceherinneringen ingesteld.", + "toast_program_armed": "Programma klaargezet voor de volgende cyclus: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Vaatwasser: stille seconden na de verwachte duur voordat de wachttijd op het wegpompen aan het einde van de cyclus wordt vrijgegeven", @@ -1093,7 +1117,9 @@ "anti_wrinkle_max_power": "Een puls boven dit niveau beëindigt anti-kreuk en opent een nieuwe cyclus", "anti_wrinkle_enabled": "De tuimelpulsen na de hoofdfase opvangen in plaats van ze als nieuwe cycli te lezen", "anti_wrinkle_max_duration": "Een puls die langer is dan dit beëindigt anti-kreuk en opent een nieuwe cyclus", - "anti_wrinkle_exit_power": "Het vermogen moet tussen de pulsen onder dit niveau vallen om anti-kreuk actief te houden" + "anti_wrinkle_exit_power": "Het vermogen moet tussen de pulsen onder dit niveau vallen om anti-kreuk actief te houden", + "anti_crease_finalize_ratio": "Deel van de verwachte duur van het herkende programma dat een cyclus moet bereiken voordat de anti-kreuknaloop mag worden afgesloten; verlaag het bij drogers met een lange naloop, laat wasmachines op de standaardwaarde", + "curve_preroll_seconds": "Seconden aan metingen uit afgebroken startpogingen die vooraan in de curve van een cyclus worden opgenomen; 0 = uit" }, "phase_desc": { "anti_crease": "Af en toe kort tuimelen na voltooiing om rimpels te verminderen.", @@ -1191,6 +1217,10 @@ "label": "Vrijgave na stille droogfase", "doc": "Zodra de cyclus zijn verwachte duur voorbij is: hoe lang de vaatwasser stil moet blijven (onder de Stopdrempel) voordat WashData niet langer op een laatste wegpompen wacht en de cyclus beëindigt. Verhoog dit als uw machine een lange stille droogfase heeft vóór een laat laatste wegpompen dat wordt gemist - een breder venster laat de geleerde duur de seizoensdrift volgen (kouder toevoerwater = langere cycli) in plaats van vast te houden aan het oude gemiddelde. Het verkort de wachttijd alleen ten opzichte van de interne limiet van 30 minuten voor de eindpiek, en verlengt die nooit." }, + "anti_crease_finalize_ratio": { + "label": "Verhouding afsluiten anti-kreuknaloop", + "doc": "Hoe ver een cyclus in de verwachte duur van het herkende programma moet zijn voordat WashData mag besluiten dat het waargenomen tuimelen met laag vermogen de anti-kreuknaloop is en de cyclus daar mag afsluiten. De verwachte duur is het gemiddelde van het programma, dus bij een droger waarvan de looptijd met vochtsensor meebeweegt met de belading haalt het merendeel van de draaien nooit 98% daarvan, en blijft de naloop staan tot de fallback-time-out - vaak 15 tot 40 minuten nadat de machine daadwerkelijk is gestopt. Verlaag deze waarde (bijv. 0,75) op zo'n droger. Laat wasmachines op de standaardwaarde staan: een wasmachine zit het grootste deel van haar cyclus onder het Max. anti-kreuk vermogen, dus deze fractie is het enige wat een rustige fase midden in de was van de naloop onderscheidt, en verlagen kan een wasbeurt halveren. Laat leeg voor de standaard (0,98). Dit is een andere instelling dan de Verhouding slimme beëindiging, die een aparte controle op vroegtijdig afsluiten regelt." + }, "anti_wrinkle_enabled": { "doc": "Herken de korte tuimelpulsen met laag vermogen die een droger afgeeft na de hoofdverwarmingsfase en houd ze vast aan het voltooide programma in plaats van ze als nieuwe cycli te lezen.", "label": "Anti-kreuk detectie inschakelen" @@ -1212,7 +1242,7 @@ "label": "Max. anti-kreuk vermogen" }, "auto_label_confidence": { - "doc": "Als de wedstrijdscore aan het einde van de cyclus op of boven dit niveau ligt, wordt het programma automatisch gelabeld zonder enige bevestigingsvraag. Verhoog het niveau om meer zekerheid te vereisen vóór automatische etikettering; verlaag het om meer te automatiseren. Werkt samen met Learning Confidence eronder.", + "doc": "Als de matchscore aan het einde van de cyclus op of boven deze waarde ligt, wordt het programma automatisch gelabeld zonder bevestigingsvraag. Verhoog de waarde om meer zekerheid te eisen vóór automatisch labelen; verlaag hem om meer te automatiseren. Werkt samen met Leerbetrouwbaarheid eronder.", "label": "Auto-label betrouwbaarheid" }, "auto_maintenance": { @@ -1223,6 +1253,10 @@ "doc": "Cycli korter dan dit worden weggegooid als spookcycli (testruns, de deur openen om een sok toe te voegen).", "label": "Min. cyclusduur" }, + "curve_preroll_seconds": { + "label": "Curve-aanloop", + "doc": "Hoeveel seconden aan metingen uit afgebroken startpogingen WashData vooraan in de curve van een cyclus mag opnemen. De curve van een cyclus begint normaal bij de startpoging die uiteindelijk slaagt, dus bij een machine die een paar keer aftast voordat ze echt begint - programmakeuze, deurvergrendeling, eerste vulling - ontbreken de eerste minuten echte activiteit in elke curve. Stel dit iets hoger in dan de langste aanloop die uw machine heeft (300 s is voor de meeste ruim voldoende). 0 schakelt het uit. Houd er rekening mee dat inschakelen het geregistreerde begin naar voren verschuift, zodat cycli van voor en na de wijziging verschillende duren dragen voor hetzelfde programma; de geleerde gemiddelden verschuiven tot de oudere cycli zijn afgevallen." + }, "delay_confirm_seconds": { "doc": "De stroom moet zo lang in de standby-band blijven voordat het apparaat wordt behandeld als wachtend om te starten in plaats van als actief.", "label": "Bevestigingsseconden" @@ -1260,7 +1294,7 @@ "label": "Eindherhalingsaantal" }, "energy_price_entity": { - "doc": "Sensor met de actuele elektriciteitsprijs per kWh (bijvoorbeeld een dynamisch tarief). Heeft voorrang op de onderstaande statische prijs. Elke cyclus bevriest de geldende prijs wanneer deze is voltooid.", + "doc": "Sensor met de actuele elektriciteitsprijs per kWh (bijvoorbeeld een dynamisch tarief). Moet een prijs zijn, niet de vermogens- of energie-entiteit van de stekker zelf: hij heeft voorrang op de onderstaande statische prijs, dus een kWh-teller hier rekent elke cyclus af tegen de meterstand in plaats van tegen je tarief. Met Tijdgewogen kosten aan wordt elke cyclus afgerekend tegen de prijs die op elk moment van de cyclus gold; anders wordt de prijs die gold toen hij klaar was erop bevroren.", "label": "Energieprijsentiteit" }, "energy_price_static": { @@ -1288,7 +1322,7 @@ "label": "Externe trigger omkeren (activeer bij UIT)" }, "learning_confidence": { - "doc": "Als de matchscore tussen dit en Auto-Label Confidence ligt, wordt u in een feedbackmelding gevraagd het geïdentificeerde programma te verifiëren. Beneden deze score is de wedstrijd te onzeker om naar boven te komen. Moet onder Auto-Label Confidence blijven.", + "doc": "Als de matchscore tussen deze waarde en Auto-label betrouwbaarheid ligt, vraagt een feedbackmelding u om het geïdentificeerde programma te verifiëren. Onder deze score is de match te onzeker om te tonen. Moet onder Auto-label betrouwbaarheid blijven.", "label": "Leerbetrouwbaarheid" }, "linked_device": { @@ -1356,7 +1390,7 @@ "label": "HA-gebeurtenissen afvuren voor meldingen" }, "notify_icon": { - "doc": "Optioneel mdi-pictogram voor de melding (bijvoorbeeld mdi:wasmachine). Laat dit leeg voor de platformstandaard.", + "doc": "Het kleine pictogram dat op de melding wordt getoond (bijvoorbeeld mdi:washing-machine). Android tekent het in de statusbalk; iOS toont het in plaats van het app-pictogram en heeft daarvoor de companion-app 2026.8 of nieuwer nodig. Laat dit leeg voor de platformstandaard.", "label": "Meldingspictogram" }, "notify_live_chronometer": { @@ -1456,7 +1490,7 @@ "label": "Afstemmingsdrempel" }, "profile_unmatch_threshold": { - "doc": "Als een live mid-cycle-wedstrijd onder deze score daalt, wordt de voorlopige identificatie gewist. Houd het iets onder de Match Threshold, zodat een korte dip in gelijkenis het scherm niet terugzet naar ongeëvenaard.", + "doc": "Als een live match tijdens de cyclus onder deze score zakt, wordt de voorlopige identificatie gewist. Houd hem iets onder de Afstemmingsdrempel, zodat een korte dip in gelijkenis het scherm niet terugzet naar geen match.", "label": "Niet-afstemmingsdrempel" }, "progress_reset_delay": { @@ -1467,10 +1501,6 @@ "doc": "Seconden dat een pomp continu kan draaien voordat deze wordt gemarkeerd als mogelijk vastgelopen (de gebeurtenis Vastgelopen pomp wordt geactiveerd).", "label": "Duur pomp vastzitten" }, - "running_dead_zone": { - "doc": "Nadat een cyclus is gestart, worden stroomonderbrekingen binnen dit venster genegeerd. Wasmachines worden gevuld met koud water (zakt tot bijna 0 W voordat ze worden verwarmd) - zonder deze bescherming lijkt de vulfase op het einde van de cyclus. Hierbij worden GEEN gegevens overgeslagen: het volledige vermogensspoor wordt geregistreerd vanaf T=0. De suggestie-engine meet het daadwerkelijke opstartpatroon van uw machine en berekent dit automatisch.", - "label": "Loopzone" - }, "sampling_interval": { "doc": "Verwachte tijd tussen sensormetingen - gebruikt om het afvlakkingsvenster te vergroten en de debounce correct te starten. Elke sensorupdate wordt ongeacht deze waarde vastgelegd; het kalibreert alleen de stroomafwaartse berekeningen. De suggestie-engine meet de werkelijke cadans van uw sensor uit eerdere cycli en stelt deze automatisch in.", "label": "Samplinginterval" @@ -1598,13 +1628,17 @@ "label": "Vermogensprofiel-interval", "doc": "Bucketgrootte voor het attribuut power_profile van de profielsensor (het platte array van gemiddelde wattwaarden per slot dat door externe planners zoals EMHASS en tibber_prices wordt gebruikt). Kleinere buckets houden korte vermogenspieken scherp; grotere buckets gladden de vorm. Standaard 15 min. Alleen voor weergave; heeft geen invloed op de detectie." }, + "notify_live_silent": { + "label": "Stille live-updates", + "doc": "Alleen iOS. Vernieuw de live-voortgang geruisloos: elke update komt binnen zonder geluid of trilling, met een lagere, batterijbesparende prioriteit die iOS even kan groeperen. De eerste update van een cyclus (die de Live activiteit start) en de start- en eindmeldingen laten nog steeds gewoon van zich horen. Zet dit uit om bij elke update een melding te krijgen." + }, "notify_live_sticky": { "label": "Live-melding bij aanraken behouden", "doc": "Alleen Android. Maak de live-voortgangsmelding aanhoudend (sticky), zodat deze bij aanraken niet wordt gesloten. Uitgeschakeld: standaardgedrag waarbij een aanraking de melding sluit." }, "notify_live_click_action": { - "label": "Aanraakdoel live-melding", - "doc": "Alleen Android. Waarheen een aanraking van de live-voortgangsmelding navigeert (bijv. /lovelace/laundry of een volledige URL) in plaats van de standaard startpagina van de app. Leeg laten voor standaard." + "label": "Aanraakdoel melding", + "doc": "Waarheen een aanraking van een melding van dit apparaat navigeert, op Android en iOS. Leeg opent het WashData-paneel bij dit apparaat. Voer een dashboardpad in (bijv. /lovelace/laundry) of een volledige URL om ergens anders naartoe te gaan, of \"none\" voor helemaal geen aanraakdoel." }, "door_opens_at_end": { "label": "Deur opent automatisch aan het einde", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Cycli die een programma vormen", "doc": "Welke cycli worden gebruikt om de vermogenscurve van elk programma op te bouwen en om een voltooide cyclus daarmee te matchen. Een soort uitvinken laat die uw programma's niet langer vormen zonder dat er iets wordt verwijderd - de cycli blijven in uw Cycli-lijst staan en kunnen nog steeds gelabeld of verwijderd worden. Handig als u geïmporteerde gegevens niet vertrouwt. Statistieken blijven ongewijzigd: die tellen altijd alleen de cycli die deze machine echt heeft uitgevoerd. Alles uitvinken wordt genegeerd, want een programma zonder cycli erachter zou nooit kunnen matchen." + }, + "energy_price_dynamic": { + "label": "Tijdgewogen kosten", + "doc": "Reken elke cyclus af tegen de prijs die op elk moment van de cyclus gold, in plaats van tegen de ene prijs die gold toen hij klaar was. Geldt alleen voor een prijsentiteit (een statische prijs kan niet bewegen). Vraagt geen extra instellingen: tijdens de cyclus wordt de prijs live bijgehouden, en \"Geschiedenis verwerken\" onder Diagnostiek berekent de kosten van oudere cycli opnieuw uit de recorder. Uit = het klassieke gedrag, één prijs bevroren aan het eind." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Splitsen…", "deleting": "Verwijderen…", "imported": "Geïmporteerd", - "preparing": "Voorbereiden…" + "preparing": "Voorbereiden…", + "unlabelling": "Label verwijderen…" }, "suggestion": { "both_agree": "WashData raadt aan", @@ -1775,10 +1814,6 @@ "higher": "Alleen zeer zekere overeenkomsten worden bevestigd – meer cycli blijven ongelabeld", "lower": "Meer cycli worden gekoppeld – sommige kunnen licht onjuist zijn" }, - "running_dead_zone": { - "higher": "Korte vermogenspieken tijdens stilstand worden genegeerd – minder ruisgevoelig", - "lower": "Reageert op kortere vermogensimpulsen – detecteert snelle transiënten" - }, "start_threshold_w": { "higher": "Voorkomt valse starts door korte vermogenspieken", "lower": "Herkent programma's met laag vermogen – kan op ruis reageren" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} schone cycli ({pct}%) hadden een interne pauze >60s die werd hervat; vereis {val} eindbevestiging(en).", "learning_confidence": "p05-betrouwbaarheid van {cycles} door de gebruiker gelabelde cycli ({p05}); daaronder wordt om verificatie gevraagd.", "match_interval": "Op basis van de waargenomen updatecadans (mediaan={median}s) * 10.", + "match_interval_capped": "Begrensd zodat {persistence} opeenvolgende herkenningen binnen {pct}% van het kortste programma ({shortest}s) passen; de updatecadans alleen (mediaan={median}s) had een langer interval voorgesteld.", + "match_interval_floored": "Alleen al het kortste programma ({shortest}s) zou dit begrenzen op {cap}s, onder het minimum van {minimum}s, dus blijft het daarop staan: {persistence} opeenvolgende matches kosten {budget}s, meer dan {pct}% van dat programma. Matchen gebeurt alleen als er een meting binnenkomt (mediaan={median}s), dus het minimum kost niets.", "max_duration_ratio": "Op basis van de duur van gelabelde cycli (p95={p95}).", "min_active": "Op basis van het minimale actieve vermogen ({min}W) dat in de laatste cyclus is waargenomen.", "min_duration_ratio": "Zo laag mogelijk gehouden zodat een programma vroeg in de cyclus wordt herkend; de betrouwbaarheids- en ambiguïteitsdrempels voorkomen voortijdige beslissingen.", @@ -1921,6 +1958,7 @@ "recording_stopped": "De opname is gestopt", "review_saved": "Beoordeling opgeslagen", "settings_conflicts": "Los de gemarkeerde instellingsconflicten op voor het opslaan.", + "settings_no_changes": "Geen wijzigingen om op te slaan", "settings_reverted": "Instellingen hersteld; integratie herladen", "settings_saved": "Instellingen opgeslagen; integratie herladen", "suggestions_applied": "Suggesties toegepast; integratie herladen", @@ -1979,7 +2017,11 @@ "export_selective_done": "Export gedownload", "import_selective_done": "{profiles} profiel(en) en {cycles} cyclus(sen) geïmporteerd", "hist_csv_required": "Laad eerst een CSV-bestand of plak de inhoud ervan", - "file_read_failed": "Kon dat bestand niet lezen" + "file_read_failed": "Kon dat bestand niet lezen", + "unlabel_done": "Label verwijderd van {count} cyclus(sen)", + "unlabel_failed": "Label verwijderen mislukt: {error}", + "odometer_saved": "Totaal aantal cycli opgeslagen", + "odometer_save_failed": "Kan totaal niet opslaan: {error}" }, "trend": { "down": "Trend naar beneden", @@ -1998,10 +2040,11 @@ "date_tip": "Datum en tijd waarop de cyclus is gestart.", "duration_tip": "Totale looptijd van de cyclus van begin tot einde.", "energy_tip": "Totaal verbruikte energie (kWh). Berekend door het vermogen over de tijd te integreren.", - "cost_tip": "Energiekosten voor deze cyclus, vastgezet bij voltooiing met de op dat moment geldende prijs (energie x prijs per kWh). Stel een prijs in onder Instellingen om deze te vullen.", + "cost_tip": "Energiekosten voor deze cyclus, bevroren bij voltooiing. Met Tijdgewogen kosten aan wordt de vermogenscurve afgerekend tegen de prijs die op elk moment gold, anders energie x de ene prijs die aan het eind gold. Stel een prijs in bij Instellingen om dit te vullen.", "confidence_tip": "Betrouwbaarheid van de profielovereenkomst (0-100%). Hoe nauwkeurig de vermogenscurve van de cyclus overeenkwam met het geïdentificeerde programma.", "health_tip": "ML-cyclusgezondheid (hoger = beter). Klik op een cyclus om deze te inspecteren en te beoordelen.", - "flags_tip": "Beoordelings-, anomalie- en bronvlaggen voor de cyclus. Beweeg de muis over een pictogram voor details." + "flags_tip": "Beoordelings-, anomalie- en bronvlaggen voor de cyclus. Beweeg de muis over een pictogram voor details.", + "cost_dynamic_tip": "Tijdgewogen: afgerekend tegen de prijs die tijdens de cyclus gold (effectief {price} {cur}/kWh)." }, "stat": { "avg": "gem.", @@ -2028,9 +2071,9 @@ "finished": "De cyclus bereikte een eindtoestand en stopte." }, "store": { + "search_ph2": "Zoeken op merk of model…", "your_model_tip": "Dit is het apparaat dat u bij Instellingen hebt opgegeven", "your_model": "Uw apparaat", - "search_brand_ph": "Zoeken op merk…", "programs_count": "Programma's: {n}", "browse": "Bladeren", "device": "Apparaat", @@ -2075,7 +2118,8 @@ "golden": "Opnieuw verwerken: referentiecycli aanvullen", "suggestions": "Opnieuw verwerken: suggesties", "ml_training": "Opnieuw verwerken: ML-training", - "health": "Opnieuw verwerken: cyclusgezondheid" + "health": "Opnieuw verwerken: cyclusgezondheid", + "costs": "Opnieuw verwerken: energiekosten" }, "pg_sweep": { "optimize": "Optimaliseren: {param}" diff --git a/custom_components/ha_washdata/translations/panel/pl.json b/custom_components/ha_washdata/translations/panel/pl.json index 2dc67e23..ab332a5f 100644 --- a/custom_components/ha_washdata/translations/panel/pl.json +++ b/custom_components/ha_washdata/translations/panel/pl.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Pod koniec ten program cichnie na około {mins} min, a następnie tuż przed zakończeniem pobiera około {watts} W przez {secs} s. Zaobserwowano w {seen} z {measured} zmierzonych cykli: urządzenie nie robi tego przy każdym uruchomieniu.", + "quiet_tail": "cicha końcówka ~{mins} min", "artifact_tip": "Wykryto {n} anomalii (np. drzwi otwarte w połowie cyklu) – otwórz, aby zobaczyć je na wykresie", "auto": "(automatycznie wykryty)", "built_in_tag": "wbudowany", @@ -77,11 +79,14 @@ "approved": "Zatwierdzone", "awaiting": "Oczekuje na zatwierdzenie", "imported_tip": "Zaimportowano ze sklepu społecznościowego. Używane tylko do dopasowywania, nie liczy się do statystyk.", + "unmatchable": "nie można dopasować", "not_importable": "niedostępne", "exists": "istnieje", - "backfilled_tip": "Wykryty w zaimportowanej historii mocy. Wpływa tylko na dopasowywanie programów, nie liczy się do statystyk." + "backfilled_tip": "Wykryty w zaimportowanej historii mocy. Wpływa tylko na dopasowywanie programów, nie liczy się do statystyk.", + "armed": "(dotyczy następnego cyklu)" }, "btn": { + "reset_zoom": "Resetuj powiększenie", "set_brand_model": "Ustaw markę i model", "refresh_catalog": "Odśwież katalog", "add_device": "+ Dodaj urządzenie", @@ -217,6 +222,8 @@ "trim": "Przytnij", "trim_split": "Przytnij / Podziel", "undo": "Cofnij", + "unlabel_selected": "Usuń etykietę z zaznaczonych ({n})", + "unlabel_selected_tip": "Usuwa etykietę profilu z zaznaczonych cykli. Pozostaną w Twojej historii i można je później oznaczyć ponownie - przestaną tylko kształtować ten profil.", "use": "Użyj", "wipe_all": "Wyczyść wszystkie dane", "delete_selected": "Usuń zaznaczone ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Zastosuj najlepsze", "pg_apply_to_settings": "Zapisz w ustawieniach", "pg_apply_to_settings_tip": "Skopiuj wartości edytowane tutaj do aktywnych ustawień tego urządzenia", - "search": "Szukaj", "import": "Importuj", "disconnect": "Rozłącz", "connect_github": "Połącz z GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Wczytaj z Home Assistant", "hist_scan": "Szukaj cykli", "hist_import_n": "Importuj cykle: {n}", - "hist_goto_cycles": "Pokaż cykle" + "hist_goto_cycles": "Pokaż cykle", + "save_total_cycles": "Zapisz łączną liczbę" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Musi być powyżej Progu Wyłączenia Zasilania ({pot} W)", "threshold": "Musi być poniżej Progu Zatrzymania ({stop} W), aby zadziałać" }, + "price_entity": { + "power_sensor": "To jest Czujnik Mocy urządzenia, a nie cena. Encja ceny ma pierwszeństwo przed statyczną ceną poniżej, więc cykle byłyby wyceniane po watach za kWh - wyczyść to pole.", + "energy_sensor": "To jest Licznik Energii urządzenia, a nie cena. Encja ceny ma pierwszeństwo przed statyczną ceną poniżej, więc cykle byłyby wyceniane po wskazaniu licznika za kWh - wyczyść to pole.", + "device_class": "Klasa tego czujnika to {dc}, a nie cena za kWh. Encja ceny ma pierwszeństwo przed statyczną ceną poniżej - wyczyść to pole lub wskaż czujnik taryfy.", + "unit": "Ten czujnik podaje wartości w {unit}, co nie jest ceną za kWh. Encja ceny ma pierwszeństwo przed statyczną ceną poniżej - wyczyść to pole lub wskaż czujnik taryfy." + }, "pump_stuck": { "duration": "Musi być mniejszy niż Limit Czasu Braku Aktualizacji ({to} s), aby alarm zacięcia zadziałał przed wyłączeniem cyklu przez watchdoga", "timeout": "Musi przekraczać Czas Zacięcia Pompy ({ps} s), aby alarm zadziałał przed wymuszonym zatrzymaniem cyklu" @@ -349,7 +362,7 @@ "pg_settings_source": "Źródło ustawień", "phase_catalog": "Katalog faz", "playground": "Piaskownica", - "process_history": "Historia procesu", + "process_history": "Przetwarzanie historii", "settings_history": "Historia ustawień", "status": "Status", "status_graph": "Wykres stanu", @@ -360,12 +373,12 @@ "wipe_history": "Wyczyść historię", "recommendations": "Zalecenia ({n})", "pg_detection_params": "Ustawienia wykrywania", - "pg_timeline": "Oś czasu zdarzeń", "pg_outcome": "Wynik symulacji", "pg_across_cycles": "We wszystkich Twoich cyklach", "community_store": "Sklep społecznościowy", "online_account": "Sklep społecznościowy i funkcje online", - "import_power_history": "Import historii mocy" + "import_power_history": "Import historii mocy", + "service_status": "Status serwisu" }, "health": { "fair": "Akceptowalna jakość profilu", @@ -373,6 +386,10 @@ "poor": "⚠ Słaba jakość profilu" }, "lbl": { + "envelope_position_tip": "Pozycja na własnej zarejestrowanej krzywej dopasowanego programu, {pct}%. Wyznaczana przez dopasowanie kształtu tego cyklu do profilu, a nie przez odliczanie czasu, więc pozostaje poprawna, gdy przebieg jest dłuższy lub krótszy niż zwykle. Odświeża się, gdy urządzenie jest ciche, więc podczas aktywnej fazy może być opóźniona.", + "envelope_position": "krzywa {pct}%", + "aria_playground_chart3": "Interaktywny wykres mocy cyklu: najedź, aby odczytać czas/moc, przewiń lub użyj dwóch palców, aby powiększyć, przeciągnij, aby przesunąć", + "device_unresolved": "Niedostępne urządzenie ({id})", "drag_to_resize": "Przeciągnij, aby zmienić rozmiar", "actions": "Działania", "activity": "Działalność", @@ -509,7 +526,7 @@ "profile_index": "indeks profilu →", "profile_name": "Nazwa profilu", "program": "Program", - "program_tip": "Zastąp profil dopasowywany do bieżącego cyklu. Automatyczne wykrywanie pozwala integracji automatycznie wybrać najlepsze dopasowanie. Przypnij określony program, aby wymusić dopasowanie, gdy automatyczne wykrywanie jest błędne lub wiesz, co jest uruchomione.", + "program_tip": "Zastąp profil dopasowywany do bieżącego cyklu. Automatyczne wykrywanie pozwala integracji automatycznie wybrać najlepsze dopasowanie. Przypnij określony program, aby wymusić dopasowanie, gdy automatyczne wykrywanie jest błędne lub wiesz, co jest uruchomione. Wybierz program przed uruchomieniem urządzenia, a zostanie zastosowany, gdy tylko rozpocznie się następny cykl.", "progress": "Postęp", "raw_socket": "Surowe gniazdo", "read_only": "Tylko do odczytu", @@ -544,6 +561,7 @@ "stage4": "Etap 4 – zgodność", "status": "Status", "tail_trim": "Przycięcie końca (s)", + "tap_to_dismiss": "dotknij wykresu, aby zamknąć", "timer_auto_pause": "Automatyczna pauza", "timer_min": "min", "timer_msg_placeholder": "Wiadomość (opcjonalnie, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Pozostały czas (model)", "pg_time_left_model_tip": "Szacowany przez model pozostały czas (estymator faz + mieszanie z ML), a nie statyczne odliczanie", "phase": "Faza", - "pg_mode_simulate": "Symuluj", "pg_mode_history": "Testuj na historii", - "pg_mode_sweep": "Przemiatanie", "pg_ev_detected": "Wykryto", "pg_ev_match": "Dopasowanie zatwierdzone", "pg_ev_match_changed": "Dopasowanie zmienione", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parametr", "pg_objective": "Cel", "pg_steps": "Kroki", - "pg_sweep_2d": "Mapa cieplna 2D", - "pg_sweep_param_y": "2. parametr", "pg_obj_match": "Trafność dopasowania", "pg_obj_endtiming": "Trafność końca", "pg_obj_falseend": "Wskaźnik fałszywych końców", "pg_obj_overrun": "Odchylenie czasu trwania", "pg_obj_ambiguity": "Wskaźnik niejednoznaczności", - "pg_best_combo": "najlepsza kombinacja", "pg_mode_optimize": "Optymalizuj", "pg_group_matching": "Dopasowywanie programów", "pg_zoom_hint": "kliknij dwukrotnie, aby zresetować powiększenie", @@ -706,7 +719,12 @@ "task_history_import_apply": "Importowanie cykli", "evidence_real_cycles": "Cykle wykonane przez to urządzenie", "evidence_reference_cycles": "Pobrane ze sklepu społecznościowego", - "evidence_backfill_cycles": "Znalezione w zaimportowanej historii mocy" + "evidence_backfill_cycles": "Znalezione w zaimportowanej historii mocy", + "zoom_hint_touch": "przybliż dwoma palcami", + "zoom_hint_touch_zoomed": "przybliż dwoma palcami · dotknij dwukrotnie, aby zresetować", + "cycles_since_service": "{since} / {total} cykli", + "total_cycles_run": "cykli łącznie", + "correct_total_cycles": "Popraw łączną liczbę" }, "log": { "all_levels": "Wszystkie poziomy", @@ -756,7 +774,7 @@ "merge_cycles": "Połącz {n} cykli", "new_group": "Nowa grupa profili", "new_phase": "Nowa faza", - "process_history_msg": "Ponownie uruchom dopasowywanie, odśwież sugestie, przetrenuj ML (jeśli włączone) i przelicz kondycję cykli dla wszystkich zapisanych cykli. Może to chwilę potrwać.", + "process_history_msg": "Ponownie uruchom dopasowywanie, odśwież sugestie, przetrenuj ML (jeśli włączone), przelicz koszty według zapisanych cen energii i przelicz kondycję cykli dla wszystkich zapisanych cykli. Może to chwilę potrwać.", "process_history_ok": "Przetwórz", "process_history_title": "Przetwarzanie historii", "process_recording": "Nagrywanie procesu", @@ -789,6 +807,7 @@ "history_import": "Import historii mocy" }, "msg": { + "pg_canvas_empty3": "Wybierz cykl powyżej i naciśnij Uruchom, aby go zasymulować. Następnie najedź, aby odczytać wartości, przewiń lub użyj dwóch palców, aby powiększyć, i przeciągnij, aby przesunąć.", "tail_trim_hint": "Ile sekund usunąć z końca", "store_sibling_hint": "Nic nie udostępniono dla Twojego dokładnego modelu? Bardzo podobny model tej samej marki to zwykle dobry punkt wyjścia.", "store_declare_appliance": "Wskaż WashData, jakie urządzenie posiadasz, a ta karta pokaże konfiguracje udostępnione dla niego przez innych użytkowników. Możesz też wpisać markę powyżej, aby się rozejrzeć.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Moc przekroczyła normalny zakres przez ~{n}s.", "auto_label_intro": "Przypisz profile do nieoznakowanych cykli, których pewność dopasowania przekracza próg.", "automations_intro": "WashData uruchamia zdarzenia {start} / {end} i udostępnia encje, więc powiadomienia i akcje najlepiej budować jako normalne automatyzacje Home Assistant. Automatyzacje korzystające z tego urządzenia są widoczne poniżej.", - "cleanup_intro": "Każdy oznaczony cykl został nałożony. Zaznacz wartości odstające i usuń, aby oczyścić profil.", + "cleanup_intro": "Wszystkie oznaczone cykle nałożone na siebie. Zaznacz odstające cykle, a następnie usuń im etykietę (zostaną w historii, ale przestaną kształtować ten profil) albo usuń je całkowicie.", "clear_debug_hint": "Usuń zapisane dane debugowania, aby zwolnić miejsce.", "collecting_data": "Zbieranie danych. Brakujące cykle do rozpoczęcia dostrajania: {need} ({current}/{min}).", "compare_overlay_profiles": "Profile nakładkowe (słabe)", @@ -902,7 +921,7 @@ "preferences_adv": "Preferencje", "prefs_personal": "Dotyczą one wyłącznie Twojego konta Home Assistant.", "font_size_hint": "Powiększ lub pomniejsz wszystko w tym panelu. Dotyczy Twojego konta na tym urządzeniu.", - "process_history_hint": "Ponownie uruchom dopasowywanie na wszystkich zapisanych cyklach, odśwież sugestie dostrajania, ponownie wytrenuj modele ML (jeśli włączone) i przelicz kondycję cykli. Uruchom po serii przeglądów.", + "process_history_hint": "Ponownie uruchom dopasowywanie na wszystkich zapisanych cyklach, odśwież sugestie dostrajania, ponownie wytrenuj modele ML (jeśli włączone), przelicz koszty cykli według zapisanych cen energii i przelicz kondycję cykli. Uruchom to po serii przeglądów.", "profile_deleted": "Profil usunięty", "profile_poor_health_detail": "Cykle przypisane do tego profilu mają niespójne kształty lub niską pewność. Rozważ przebudowę koperty lub przejrzenie oznakowanych cykli.", "profiles_intro": "Kliknij profil, aby zobaczyć statystyki, fazy i porządki. Grupuj niemal identyczne programy (ten sam kształt/czas trwania, różna temperatura lub wirowanie), aby dopasowanie umożliwiało niezawodne wybieranie między nimi.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "'{name}' ma niski wskaźnik dopasowania - jego ostatnie cykle mocno się różnią lub słabo pasują. Przejrzyj jego cykle lub nagraj profil ponownie, aby rozpoznawanie i szacunki czasu pozostały dokładne.", "advisory_shape_drift": "'{name}' znacząco odbiegł od pierwotnego kształtu krzywej mocy. Zachowanie urządzenia mogło zmienić się z czasem (np. kamień, zużycie). Rozważ ponowne nagranie tego profilu na podstawie ostatnich cykli.", "advisory_shape_drift_corr": "'{name}' znacząco odbiegł od pierwotnego kształtu krzywej mocy (korelacja {corr}). Zachowanie urządzenia mogło zmienić się z czasem (np. kamień, zużycie). Rozważ ponowne nagranie tego profilu na podstawie ostatnich cykli.", + "advisory_unmatchable": "'{name}' nigdy nie zostanie dopasowany: nie stoi za nim żaden cykl z danymi o mocy, więc WashData nie ma z czym porównać trwającego cyklu. Oznacz jakiś cykl tym programem (lub nagraj jeden), aby profil wrócił do dopasowywania.", + "advisory_unmatchable_no_duration": "'{name}' nigdy nie zostanie dopasowany: ma cykl z danymi o mocy, ale bez użytecznego czasu trwania, więc WashData nie ma długości odniesienia, według której mierzy dopasowanie. Nagraj ten program ponownie lub ustaw jego oczekiwany czas trwania, aby wrócił do dopasowywania.", "pg_no_alerts": "W tym uruchomieniu nie wykryto żadnych problemów.", "pg_override_active": "Używane są Twoje edytowane ustawienia zamiast bieżących", - "pg_history_intro": "Odtwórz swoje ostatnie cykle przez rzeczywisty detektor i mechanizm dopasowania. Najpierw edytuj ustawienie wykrywania w trybie Symuluj, aby zobaczyć tu porównanie przed/po.", "pg_history_empty": "Naciśnij Uruchom, aby odtworzyć swoje cykle.", "pg_history_summary": "{detected}/{total} wykryto · {correct} poprawnie dopasowano", "pg_sweep_no_metric": "Za mało danych, aby ocenić ten cel.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Skanowanie nie powiodło się.", "hist_scan_expired": "To skanowanie nie jest już dostępne. Uruchom je ponownie.", "hist_import_failed": "Import nie powiódł się.", - "imported_history_readonly": "Wykryty w zaimportowanej historii mocy. Wpływa na dopasowywanie programów, ale nie liczy się do Twoich statystyk i nie można go przyciąć ani podzielić. Oznacz go, aby nadać mu nazwę programu." + "imported_history_readonly": "Wykryty w zaimportowanej historii mocy. Wpływa na dopasowywanie programów, ale nie liczy się do Twoich statystyk i nie można go przyciąć ani podzielić. Oznacz go, aby nadać mu nazwę programu.", + "odometer_intro": "Ta łączna liczba tylko rośnie. Usunięcie zapisu cyklu jej nie zmienia, a licznik działa dalej także po przekroczeniu limitu przechowywanej historii, więc przypomnienia serwisowe pozostają prawidłowe.", + "odometer_correct_hint": "Ustaw tę wartość, jeśli WashData zapisał cykl, który nigdy się nie odbył, albo jeśli urządzenie wykonało już cykle przed instalacją WashData.", + "no_reminders_set": "Nie ustawiono jeszcze żadnych przypomnień serwisowych.", + "toast_program_armed": "Program ustawiony na następny cykl: {program}" }, "phase_desc": { "anti_crease": "Sporadyczne krótkie obroty bębna po zakończeniu w celu zapobiegania gnieceniu tkanin.", @@ -1161,6 +1185,10 @@ "label": "Zwolnienie po cichej fazie suszenia", "doc": "Gdy cykl przekroczy oczekiwany czas trwania, jak długo zmywarka musi pozostać cicha (poniżej Progu Zatrzymania), zanim WashData przestanie czekać na końcowe odpompowanie wody i zakończy cykl. Zwiększ tę wartość, jeśli Twoje urządzenie ma długą cichą fazę suszenia przed późnym końcowym odpompowaniem, które jest pomijane - szersze okno pozwala, aby wyuczony czas trwania podążał za sezonowym dryfem (chłodniejsza woda na wlocie = dłuższe cykle), zamiast trzymać się starej średniej. Może jedynie skrócić oczekiwanie względem wewnętrznego 30-minutowego limitu końcowego odpompowania, nigdy go nie wydłuża." }, + "anti_crease_finalize_ratio": { + "doc": "Jak daleko w oczekiwanym czasie trwania dopasowanego programu musi znajdować się cykl, zanim WashData może uznać obserwowane obracanie bębna o niskiej mocy za antygniotowy ogon i zakończyć w tym miejscu cykl. Oczekiwany czas trwania to średnia programu, więc w suszarce, której czas suszenia z czujnikiem wilgotności zależy od wsadu, większość uruchomień nigdy nie osiąga 98% tej wartości, a ogon ciągnie się aż do awaryjnego limitu czasu - często 15 do 40 minut po tym, jak maszyna faktycznie się zatrzymała. W takiej suszarce obniż tę wartość (np. 0,75). W pralkach pozostaw wartość domyślną: pralka spędza większość cyklu poniżej ustawienia Maks. Moc Antygniotowa, więc ten ułamek jest jedyną rzeczą oddzielającą cichą fazę w środku prania od ogona, a jego obniżenie może skrócić pranie o połowę. Pozostaw puste dla wartości domyślnej (0,98). To inne ustawienie niż Współczynnik inteligentnego zakończenia, które steruje osobnym sprawdzeniem wcześniejszego zakończenia.", + "label": "Współczynnik zakończenia trybu antygniotowego" + }, "anti_wrinkle_enabled": { "doc": "Rozpoznawaj krótkie impulsy suszenia o małej mocy, które suszarka emituje po głównej fazie nagrzewania, i pamiętaj o ich dołączeniu do zakończonego cyklu, zamiast odczytywać je jako nowe cykle.", "label": "Włącz Wykrywanie Antygniotowe" @@ -1230,7 +1258,7 @@ "label": "Liczba Powtórzeń Zakończenia" }, "energy_price_entity": { - "doc": "Czujnik z aktualną ceną energii elektrycznej za kWh (np. taryfa dynamiczna). Ma pierwszeństwo przed ceną statyczną poniżej. Każdy cykl zamraża cenę obowiązującą w momencie jego zakończenia.", + "doc": "Sensor z aktualną ceną energii elektrycznej za kWh (np. taryfa dynamiczna). Musi to być cena, a nie własna encja mocy lub energii wtyczki: ma pierwszeństwo przed statyczną ceną poniżej, więc licznik kWh wskazany tutaj wycenia każdy cykl według wskazania licznika zamiast Twojej taryfy. Gdy Koszt ważony czasem jest włączony, każdy cykl jest naliczany według ceny obowiązującej w każdej chwili jego trwania; w przeciwnym razie zostaje na nim zamrożona cena obowiązująca w momencie zakończenia.", "label": "Encja Ceny Energii" }, "energy_price_static": { @@ -1258,7 +1286,7 @@ "label": "Odwróć Zewnętrzny Wyzwalacz (wyzwalaj przy WYŁ)" }, "learning_confidence": { - "doc": "Jeżeli wynik dopasowania mieści się w przedziale pomiędzy tą a Pewnością automatycznej etykiety, zostanie wyświetlone powiadomienie zwrotne z prośbą o zweryfikowanie zidentyfikowanego programu. Poniżej tego wyniku mecz jest zbyt niepewny, aby go wystawić na światło dzienne. Musi być utrzymywany poniżej poziomu pewności automatycznej etykiety.", + "doc": "Jeśli wynik dopasowania mieści się pomiędzy tą wartością a Pewnością Automatycznego Etykietowania, powiadomienie zwrotne poprosi o zweryfikowanie zidentyfikowanego programu. Poniżej tego wyniku dopasowanie jest zbyt niepewne, aby je pokazywać. Musi pozostać poniżej Pewności Automatycznego Etykietowania.", "label": "Pewność Uczenia" }, "linked_device": { @@ -1326,7 +1354,7 @@ "label": "Wysyłaj Zdarzenia HA dla Powiadomień" }, "notify_icon": { - "doc": "Opcjonalna ikona mdi dla powiadomienia (np. mdi:washer-machine). Pozostaw puste dla domyślnej platformy.", + "doc": "Mała ikona wyświetlana w powiadomieniu (np. mdi:washing-machine). Android rysuje ją na pasku stanu; iOS pokazuje ją zamiast ikony aplikacji i wymaga aplikacji mobilnej Home Assistant (companion app) w wersji 2026.8 lub nowszej. Pozostaw puste, aby użyć domyślnej ikony platformy.", "label": "Ikona Powiadomienia" }, "notify_live_chronometer": { @@ -1426,7 +1454,7 @@ "label": "Próg Dopasowania" }, "profile_unmatch_threshold": { - "doc": "Jeśli wynik meczu na żywo w połowie cyklu spadnie poniżej tego wyniku, wstępna identyfikacja zostanie wyczyszczona. Trzymaj go nieco poniżej progu dopasowania, aby krótki spadek podobieństwa nie spowodował powrotu do stanu niezrównanego.", + "doc": "Jeśli dopasowanie na żywo w trakcie cyklu spadnie poniżej tego wyniku, wstępna identyfikacja zostanie wyczyszczona. Trzymaj je nieco poniżej Progu Dopasowania, aby krótki spadek podobieństwa nie przełączył wyświetlania z powrotem na brak dopasowania.", "label": "Próg Niedopasowania" }, "progress_reset_delay": { @@ -1437,10 +1465,6 @@ "doc": "Przez kilka sekund pompa może pracować nieprzerwanie, zanim zostanie oznaczona jako prawdopodobnie zablokowana (wywołane zostanie zdarzenie zablokowania pompy).", "label": "Czas Zacięcia Pompy" }, - "running_dead_zone": { - "doc": "Po rozpoczęciu cyklu spadki mocy w tym oknie są ignorowane. Pralki napełniają się zimną wodą (spadającą w okolicach 0 W przed nagrzaniem) - bez tego zabezpieczenia faza napełniania wygląda jak koniec cyklu. NIE powoduje to pominięcia danych: zapis pełnej mocy jest rejestrowany od T=0. Silnik sugestii mierzy rzeczywisty wzorzec uruchamiania Twojej maszyny i automatycznie go dopasowuje.", - "label": "Strefa Martwa" - }, "sampling_interval": { "doc": "Oczekiwany czas pomiędzy odczytami czujnika – używany do określenia rozmiaru okna wygładzającego i prawidłowego rozpoczęcia odbijania. Każda aktualizacja czujnika jest rejestrowana niezależnie od tej wartości; kalibruje jedynie dalsze obliczenia. Silnik sugestii mierzy rzeczywistą kadencję czujnika z poprzednich cykli i ustawia ją automatycznie.", "label": "Interwał Próbkowania" @@ -1568,13 +1592,17 @@ "label": "Interwał profilu mocy", "doc": "Rozmiar segmentu dla atrybutu senzora power_profile na profil (płaska tablica średnich watów na slot zużywana przez zewnętrzne planerzy jak EMHASS i tibber_prices). Mniejsze segmenty zachowują krótkie skoki mocy ostre; większe segmenty wygładzają kształt. Domyślnie 15 min. Tylko do odczytu; nie wpływa na wykrywanie." }, + "notify_live_silent": { + "label": "Ciche aktualizacje na żywo", + "doc": "Tylko iOS. Odświeżaj postęp na żywo po cichu: każda aktualizacja przychodzi bez dźwięku i wibracji, z niższym priorytetem oszczędzającym baterię, więc iOS może je na krótko grupować. Pierwsza aktualizacja cyklu (ta, która uruchamia Live Activity) oraz powiadomienia o rozpoczęciu i zakończeniu nadal powiadamiają jak zwykle. Wyłącz, aby otrzymywać sygnał przy każdej aktualizacji." + }, "notify_live_sticky": { "label": "Zachowaj powiadomienie na żywo po dotknięciu", "doc": "Tylko Android. Ustaw powiadomienie o postępie na żywo jako trwałe (przypięte), aby dotknięcie nie odrzucało aktywnego wątku. Wyłączone zachowuje domyślne zachowanie, gdzie dotknięcie odrzuca powiadomienie." }, "notify_live_click_action": { - "label": "Cel dotknięcia powiadomienia na żywo", - "doc": "Tylko Android. Gdzie dotknięcie powiadomienia o postępie na żywo otwiera (np. /lovelace/laundry lub pełny URL) zamiast strony startowej aplikacji. Pozostaw puste dla wartości domyślnej." + "label": "Cel dotknięcia powiadomienia", + "doc": "Gdzie prowadzi dotknięcie powiadomienia z tego urządzenia, w systemach Android i iOS. Puste pole otwiera panel WashData na tym urządzeniu. Wpisz ścieżkę pulpitu nawigacyjnego (np. /lovelace/laundry) lub pełny URL, aby otworzyć coś innego, albo „none”, aby dotknięcie nie otwierało niczego." }, "door_opens_at_end": { "label": "Drzwi otwierają się automatycznie na końcu", @@ -1587,6 +1615,14 @@ "profile_evidence_sources": { "label": "Cykle kształtujące program", "doc": "Które cykle są używane do zbudowania krzywej mocy każdego programu i do dopasowania do niej zakończonego cyklu. Odznaczenie danego rodzaju sprawia, że przestaje on wpływać na Twoje programy, ale nic nie zostaje usunięte - cykle pozostają na liście Cykle i nadal można je oznaczyć lub usunąć. Przydatne, jeśli nie masz zaufania do zaimportowanych danych. Nie ma to wpływu na statystyki: zawsze liczą tylko cykle, które to urządzenie faktycznie wykonało. Odznaczenie wszystkiego jest ignorowane, ponieważ program bez żadnych cykli nigdy nie mógłby zostać dopasowany." + }, + "energy_price_dynamic": { + "label": "Koszt ważony czasem", + "doc": "Nalicza każdy cykl według ceny obowiązującej w każdej chwili jego trwania, zamiast według jednej ceny z momentu zakończenia. Działa tylko z encją ceny (cena statyczna nie może się zmieniać). Nie wymaga dodatkowej konfiguracji: cena jest śledzona na bieżąco w trakcie cyklu, a Przetwarzanie historii w Diagnostyce przelicza koszty starszych cykli z recordera. Wyłączone = klasyczne działanie, jedna cena zamrożona na koniec." + }, + "curve_preroll_seconds": { + "doc": "Ile sekund odczytów z przerwanych prób startu WashData może dołączyć na początku krzywej cyklu. Krzywa cyklu zwykle zaczyna się od tej próby startu, która ostatecznie się powiodła, więc w maszynie, która próbuje kilka razy, zanim się rozkręci - wybór programu, blokada drzwi, pierwsze napełnianie - w każdej krzywej brakuje pierwszych minut rzeczywistej pracy. Ustaw tę wartość nieco powyżej najdłuższego takiego rozbiegu Twojej maszyny (300 s w zupełności wystarczy w większości przypadków). 0 wyłącza tę funkcję. Pamiętaj, że jej włączenie przesuwa zapisany początek wcześniej, więc cykle zapisane przed zmianą i po niej mają różne czasy trwania dla tego samego programu; wyuczone średnie będą się przesuwać, dopóki starsze cykle nie wypadną z historii.", + "label": "Wstępny zapis krzywej" } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Dzielenie…", "deleting": "Usuwanie…", "imported": "Zaimportowano", - "preparing": "Przygotowywanie…" + "preparing": "Przygotowywanie…", + "unlabelling": "Usuwanie etykiety…" }, "suggestion": { "both_agree": "WashData zaleca", @@ -1745,10 +1782,6 @@ "higher": "Zatwierdza tylko dopasowania o wysokiej pewności; więcej cykli pozostaje nieoznakowanych", "lower": "Dopasowuje więcej cykli; niektóre mogą być lekko błędne" }, - "running_dead_zone": { - "higher": "Ignoruje krótkie skoki mocy podczas bezczynności – mniej wrażliwy na zakłócenia", - "lower": "Reaguje na krótsze impulsy mocy – wykrywa szybką aktywność przejściową" - }, "start_threshold_w": { "higher": "Zapobiega fałszywym startom z krótkich skoków mocy", "lower": "Wykrywa programy o niskiej mocy; może reagować na zakłócenia" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} poprawnych cykli ({pct}%) miało wewnętrzną pauzę >60s, która została wznowiona; wymagaj {val} potwierdzeń zakończenia.", "learning_confidence": "Pewność p05 dla {cycles} cykli oznaczonych przez użytkownika ({p05}); poniżej tej wartości żądaj weryfikacji.", "match_interval": "Na podstawie obserwowanej częstotliwości aktualizacji (mediana={median}s) * 10.", + "match_interval_capped": "Ograniczone tak, aby {persistence} kolejnych dopasowań zmieściło się w {pct}% najkrótszego programu ({shortest}s); sama częstotliwość aktualizacji (mediana={median}s) sugerowałaby dłuższy interwał.", + "match_interval_floored": "Sam najkrótszy program ({shortest}s) ograniczyłby tę wartość do {cap}s, poniżej minimum {minimum}s, więc jest ona utrzymywana na minimum: seria {persistence} kolejnych dopasowań zajmuje {budget}s, czyli więcej niż {pct}% tego programu. Dopasowywanie uruchamia się tylko wtedy, gdy nadejdzie nowy odczyt (mediana={median}s), więc minimum nie kosztuje nic.", "max_duration_ratio": "Na podstawie czasów trwania oznaczonych cykli (p95={p95}).", "min_active": "Na podstawie minimalnej mocy czynnej ({min}W) zaobserwowanej w ostatnim cyklu.", "min_duration_ratio": "Utrzymane jak najniżej, aby program był rozpoznawany na wczesnym etapie cyklu; progi pewności i niejednoznaczności zapobiegają przedwczesnym decyzjom.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Nagrywanie zatrzymane", "review_saved": "Recenzja zapisana", "settings_conflicts": "Napraw zaznaczone konflikty ustawień przed zapisem.", + "settings_no_changes": "Brak zmian do zapisania", "settings_reverted": "Ustawienia przywrócono; integracja jest przeładowywana", "settings_saved": "Ustawienia zapisane; przeładowanie integracji", "suggestions_applied": "Zastosowano sugestie; przeładowanie integracji", @@ -1949,7 +1985,11 @@ "export_selective_done": "Eksport pobrany", "import_selective_done": "Zaimportowano {profiles} profil(e) i {cycles} cykl(e)", "hist_csv_required": "Najpierw wczytaj plik CSV lub wklej jego zawartość", - "file_read_failed": "Nie udało się odczytać tego pliku" + "file_read_failed": "Nie udało się odczytać tego pliku", + "unlabel_done": "Usunięto etykietę z {count} cykli", + "unlabel_failed": "Usunięcie etykiety nie powiodło się: {error}", + "odometer_saved": "Łączna liczba cykli zapisana", + "odometer_save_failed": "Nie udało się zapisać łącznej liczby: {error}" }, "trend": { "down": "Trend w dół", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Zmywarka: sekundy ciszy po oczekiwanym czasie trwania, zanim zwolnione zostanie oczekiwanie na końcowe odpompowanie wody", "smart_termination_duration_ratio": "Ułamek oczekiwanego czasu trwania dopasowanego programu, który cykl musi osiągnąć, zanim Inteligentne zakończenie może go zakończyć wcześniej; obniż go w przypadku maszyn zależnych od wsadu lub temperatury", + "anti_crease_finalize_ratio": "Ułamek oczekiwanego czasu trwania dopasowanego programu, który cykl musi osiągnąć, zanim antygniotowy ogon obracania bębna może zostać zakończony; obniż go w suszarkach z długim ogonem obracania, w pralkach pozostaw wartość domyślną", "anti_wrinkle_enabled": "Pochłaniaj impulsy bębna po głównej fazie, zamiast odczytywać je jako nowe cykle", "anti_wrinkle_exit_power": "Moc musi spaść poniżej tej wartości pomiędzy impulsami, aby tryb antygniotowy pozostał aktywny", "anti_wrinkle_idle_timeout": "Dozwolony czas ciszy między dwoma impulsami bębna, zanim tryb antygniotowy się zakończy", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Etap 3: odległość półnasycenia DDTW; mniejsza = bardziej wrażliwa na kształt (domyślnie 30)", "dtw_refine_top_n": "Etap 3: kandydaci ponownie oceniani przez DTW; zwiększ do 7-9, jeśli właściwy profil zajmuje 4-5 miejsce (domyślnie 5)", "duration_scale": "Etap 4: logarytm stosunku, przy którym zgodność czasu trwania spada do połowy; mniejszy = surowsza kara (domyślnie 0,175)", - "energy_scale": "Etap 4: logarytm stosunku, przy którym zgodność energii spada do połowy; mniejszy = surowsza kara (domyślnie 0,25)" + "energy_scale": "Etap 4: logarytm stosunku, przy którym zgodność energii spada do połowy; mniejszy = surowsza kara (domyślnie 0,25)", + "curve_preroll_seconds": "Sekundy odczytów z przerwanych prób startu dołączane na początku krzywej cyklu; 0 = wyłączone" }, "col": { "profile_tip": "Nazwa dopasowanego programu. Brak etykiety oznacza, że na końcu cyklu nie dopasowano żadnego profilu.", @@ -1998,10 +2040,11 @@ "date_tip": "Data i godzina rozpoczęcia cyklu.", "duration_tip": "Całkowity czas działania cyklu od początku do końca.", "energy_tip": "Całkowite zużycie energii (kWh). Obliczone przez całkowanie mocy w czasie.", - "cost_tip": "Koszt energii dla tego cyklu, ustalony w momencie zakończenia na podstawie ceny obowiązującej wtedy (energia x cena za kWh). Ustaw cenę w Ustawieniach, aby go wypełnić.", + "cost_tip": "Koszt energii dla tego cyklu, zamrożony w chwili zakończenia. Gdy Koszt ważony czasem jest włączony, krzywa mocy jest naliczana według ceny obowiązującej w każdej chwili; w przeciwnym razie energia x jedna cena obowiązująca na koniec. Ustaw cenę w Ustawieniach, aby wartość się pojawiła.", "confidence_tip": "Pewność dopasowania profilu (0-100%). Jak dokładnie krzywa mocy cyklu dopasowała się do zidentyfikowanego programu.", "health_tip": "Kondycja cyklu ML (wyższa = lepsza). Kliknij cykl, aby go sprawdzić i ocenić.", - "flags_tip": "Flagi społeczności i wskaźniki jakości danych" + "flags_tip": "Flagi społeczności i wskaźniki jakości danych", + "cost_dynamic_tip": "Ważone czasem: naliczone według ceny obowiązującej w trakcie cyklu (efektywnie {price} {cur}/kWh)." }, "stat": { "avg": "śr.", @@ -2028,9 +2071,9 @@ "finished": "Cykl osiągnął stan końcowy i zakończył się." }, "store": { + "search_ph2": "Szukaj według marki lub modelu…", "your_model_tip": "To urządzenie wskazane przez Ciebie w Ustawieniach", "your_model": "Twoje", - "search_brand_ph": "Szukaj według marki…", "programs_count": "Programy: {n}", "browse": "Przeglądaj", "device": "Urządzenie", @@ -2075,7 +2118,8 @@ "golden": "Ponowne przetwarzanie: uzupełnianie referencyjnych", "suggestions": "Ponowne przetwarzanie: sugestie", "ml_training": "Ponowne przetwarzanie: trenowanie ML", - "health": "Ponowne przetwarzanie: zdrowie cykli" + "health": "Ponowne przetwarzanie: zdrowie cykli", + "costs": "Ponowne przetwarzanie: koszty energii" }, "pg_sweep": { "optimize": "Optymalizacja: {param}" diff --git a/custom_components/ha_washdata/translations/panel/pt-BR.json b/custom_components/ha_washdata/translations/panel/pt-BR.json index 8df517f0..d06ee635 100644 --- a/custom_components/ha_washdata/translations/panel/pt-BR.json +++ b/custom_components/ha_washdata/translations/panel/pt-BR.json @@ -1,5 +1,7 @@ { "badge": { + "quiet_tail_tip": "Perto do fim, este programa fica em silêncio por cerca de {mins} min e depois consome cerca de {watts} W durante {secs} s antes de terminar. Observado em {seen} de {measured} ciclos medidos: o eletrodoméstico não faz isso em todas as execuções.", + "quiet_tail": "silêncio final ~{mins} min", "artifact_tip": "{n} anomalias detectadas (por exemplo, porta aberta no meio do ciclo) – abra para vê-las no gráfico", "auto": "(detectado automaticamente)", "built_in_tag": "integrado", @@ -29,11 +31,14 @@ "approved": "Aprovado", "awaiting": "Aguardando aprovação", "imported_tip": "Importado da loja da comunidade. Usado apenas para correspondência, não contabilizado nas estatísticas.", + "unmatchable": "correspondência impossível", "not_importable": "n/d aqui", "exists": "já existe", - "backfilled_tip": "Detectado em um histórico de potência importado. Influencia apenas a correspondência de programas, não é contabilizado nas estatísticas." + "backfilled_tip": "Detectado em um histórico de potência importado. Influencia apenas a correspondência de programas, não é contabilizado nas estatísticas.", + "armed": "(aplica-se ao próximo ciclo)" }, "btn": { + "reset_zoom": "Redefinir zoom", "set_brand_model": "Definir marca e modelo", "refresh_catalog": "Atualizar catálogo", "add_device": "+ Adicionar dispositivo", @@ -167,6 +172,8 @@ "trim": "Recortar", "trim_split": "Recortar / Dividir", "undo": "Desfazer", + "unlabel_selected": "Remover rótulo dos selecionados ({n})", + "unlabel_selected_tip": "Remove o rótulo de perfil dos ciclos selecionados. Eles continuam no seu histórico e poderão ser rotulados novamente depois; apenas deixam de influenciar este perfil.", "use": "Usar", "wipe_all": "Apagar todos os dados", "delete_selected": "Excluir selecionados ({n})", @@ -174,7 +181,6 @@ "pg_apply_best": "Aplicar melhor", "pg_apply_to_settings": "Salvar nas configurações", "pg_apply_to_settings_tip": "Copiar os valores que você editou aqui para as configurações ativas deste dispositivo", - "search": "Pesquisar", "import": "Importar", "disconnect": "Desconectar", "connect_github": "Conectar GitHub", @@ -201,7 +207,8 @@ "hist_read_recorder": "Ler do Home Assistant", "hist_scan": "Procurar ciclos", "hist_import_n": "Importar {n} ciclos", - "hist_goto_cycles": "Ver os ciclos" + "hist_goto_cycles": "Ver os ciclos", + "save_total_cycles": "Salvar total" }, "conflict": { "anti_wrinkle_exit": { @@ -214,6 +221,12 @@ }, "attn_sub": "Corrija os conflitos antes de salvar", "attn_title": "Conflitos de configurações: {n}", + "price_entity": { + "power_sensor": "Este é o Sensor de Potência do dispositivo, não um preço. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo, então os ciclos seriam cobrados a watts por kWh - limpe este campo.", + "energy_sensor": "Esta é a Entidade do Medidor de Energia do dispositivo, não um preço. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo, então os ciclos seriam cobrados pela leitura do medidor por kWh - limpe este campo.", + "device_class": "Este sensor mede {dc}, não um preço por kWh. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo - limpe este campo ou aponte-o para um sensor de tarifa.", + "unit": "Este sensor indica valores em {unit}, o que não é um preço por kWh. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo - limpe este campo ou aponte-o para um sensor de tarifa." + }, "settings_banner": "Conflitos de configurações: {n}. Verifique as seções destacadas e corrija-os antes de salvar.", "settings_banner_btn": "Ir para o primeiro", "confidence": { @@ -301,7 +314,7 @@ "pg_settings_source": "Origem das configurações", "phase_catalog": "Catálogo de fases", "playground": "Área de testes", - "process_history": "Histórico de processamento", + "process_history": "Processar histórico", "settings_history": "Histórico de configurações", "status": "Status", "status_graph": "Gráfico de status", @@ -312,12 +325,12 @@ "wipe_history": "Apagar histórico", "recommendations": "Recomendações ({n})", "pg_detection_params": "Configurações de detecção", - "pg_timeline": "Linha do tempo de eventos", "pg_outcome": "Resultado da simulação", "pg_across_cycles": "Em todos os seus ciclos", "community_store": "Loja da comunidade", "online_account": "Loja da comunidade e recursos online", - "import_power_history": "Importar histórico de potência" + "import_power_history": "Importar histórico de potência", + "service_status": "Status da manutenção" }, "health": { "fair": "Qualidade de perfil aceitável", @@ -325,6 +338,10 @@ "poor": "⚠ Qualidade de perfil fraca" }, "lbl": { + "envelope_position_tip": "Posição na curva gravada do próprio programa correspondente, {pct}%. É medida alinhando este ciclo com o perfil em vez de contar o tempo, por isso continua certa quando um ciclo é mais longo ou mais curto que o normal. É atualizada enquanto o eletrodoméstico está em silêncio, então pode ficar atrasada durante uma fase ativa.", + "envelope_position": "curva {pct}%", + "aria_playground_chart3": "Gráfico interativo de potência do ciclo: passe o cursor para ler tempo/potência, role ou use o gesto de pinça para ampliar, arraste para deslocar", + "device_unresolved": "Dispositivo indisponível ({id})", "drag_to_resize": "Arraste para redimensionar", "actions": "Ações", "activity": "Atividade", @@ -460,7 +477,7 @@ "profile_index": "índice do perfil →", "profile_name": "Nome do perfil", "program": "Programa", - "program_tip": "Substitua o perfil correspondido ao ciclo atual. A detecção automática permite que a integração escolha a melhor correspondência automaticamente. Fixe um programa específico para forçar a correspondência quando a detecção automática estiver errada ou você souber o que está em execução.", + "program_tip": "Substitua o perfil correspondido ao ciclo atual. A detecção automática permite que a integração escolha a melhor correspondência automaticamente. Fixe um programa específico para forçar a correspondência quando a detecção automática estiver errada ou você souber o que está em execução. Escolha um antes de ligar o eletrodoméstico e ele será aplicado assim que o próximo ciclo começar.", "progress": "Progresso", "raw_socket": "Socket bruto", "read_only": "Somente leitura", @@ -495,6 +512,7 @@ "stage4": "Etapa 4 – concordância", "status": "Status", "tail_trim": "Recorte final (s)", + "tap_to_dismiss": "toque no gráfico para fechar", "timer_auto_pause": "Pausa automática", "timer_min": "min", "timer_msg_placeholder": "Mensagem (opcional, {device}/{program}/{minutes})", @@ -531,9 +549,7 @@ "pg_time_left_model": "Tempo restante (modelo)", "pg_time_left_model_tip": "Tempo restante estimado pelo modelo (estimador de fase + combinação de ML), não uma contagem regressiva fixa", "phase": "Fase", - "pg_mode_simulate": "Simular", "pg_mode_history": "Testar no histórico", - "pg_mode_sweep": "Varredura", "pg_ev_detected": "Detectado", "pg_ev_match": "Correspondência confirmada", "pg_ev_match_changed": "Correspondência alterada", @@ -572,14 +588,11 @@ "pg_stress_idle_w_desc": "Substitui o nível mínimo de espera detectado automaticamente (deixe em branco para automático).", "pg_stress_toggle": "Testar encerramento em repouso", "pg_stress_toggle_desc": "Simula o eletrodoméstico mantido no seu consumo em espera após o fim da gravação – mostra se e quando o WashData encerra o ciclo.", - "pg_sweep_2d": "Mapa de calor 2D", - "pg_sweep_param_y": "2º parâmetro", "pg_obj_match": "Precisão de correspondência", "pg_obj_endtiming": "Precisão do momento de fim", "pg_obj_falseend": "Taxa de fins falsos", "pg_obj_overrun": "Desvio de duração", "pg_obj_ambiguity": "Taxa de ambiguidade", - "pg_best_combo": "melhor combinação", "pg_mode_optimize": "Otimizar", "pg_group_matching": "Correspondência de programa", "pg_zoom_hint": "clique duas vezes para redefinir o zoom", @@ -658,7 +671,12 @@ "task_history_import_apply": "Importando ciclos", "evidence_real_cycles": "Ciclos que esta máquina executou", "evidence_reference_cycles": "Baixados da loja da comunidade", - "evidence_backfill_cycles": "Encontrados em um histórico de potência importado" + "evidence_backfill_cycles": "Encontrados em um histórico de potência importado", + "zoom_hint_touch": "gesto de pinça para dar zoom", + "zoom_hint_touch_zoomed": "gesto de pinça para dar zoom · toque duplo para redefinir", + "cycles_since_service": "{since} / {total} ciclos", + "total_cycles_run": "ciclos realizados no total", + "correct_total_cycles": "Corrigir o total" }, "log": { "all_levels": "Todos os níveis", @@ -708,7 +726,7 @@ "merge_cycles": "Mesclar {n} ciclos", "new_group": "Novo grupo de perfis", "new_phase": "Nova fase", - "process_history_msg": "Reexecutar correspondência, atualizar sugestões, retreinar ML (se habilitado) e recalcular a saúde dos ciclos em todos os ciclos armazenados. Isso pode demorar um pouco.", + "process_history_msg": "Reexecutar correspondência, atualizar sugestões, retreinar ML (se habilitado), recalcular o custo dos ciclos com base nos preços de energia registrados e recalcular a saúde dos ciclos em todos os ciclos armazenados. Isso pode demorar um pouco.", "process_history_ok": "Processar", "process_history_title": "Processar Histórico", "process_recording": "Processar gravação", @@ -741,6 +759,7 @@ "history_import": "Importar histórico de potência" }, "msg": { + "pg_canvas_empty3": "Escolha um ciclo acima e pressione Executar para simulá-lo. Depois passe o cursor para ler os valores, role ou use o gesto de pinça para ampliar e arraste para deslocar.", "tail_trim_hint": "Número de segundos a remover do final", "store_sibling_hint": "Nada compartilhado para o seu modelo exato? Um modelo muito parecido da mesma marca costuma ser um bom ponto de partida.", "store_declare_appliance": "Informe ao WashData qual eletrodoméstico você tem e esta aba mostrará as configurações que outras pessoas compartilharam para ele. Você também pode digitar uma marca acima para dar uma olhada.", @@ -754,7 +773,7 @@ "artifact_spike_detail": "Ultrapassou a banda de potência habitual por ~{n}s.", "auto_label_intro": "Atribua perfis a ciclos sem rótulo cuja confiança de correspondência supere o limite.", "automations_intro": "O WashData dispara eventos {start} / {end} e expõe entidades, portanto as notificações e ações são melhor criadas como automatizações normais do Home Assistant. As automatizações que usam este dispositivo aparecem abaixo.", - "cleanup_intro": "Todos os ciclos rotulados sobrepostos. Marque os valores discrepantes e exclua para limpar o perfil.", + "cleanup_intro": "Todos os ciclos rotulados sobrepostos. Marque os valores discrepantes e depois remova o rótulo deles (permanecem no histórico, mas deixam de influenciar este perfil) ou exclua-os de vez.", "clear_debug_hint": "Remova os dados de depuração armazenados para liberar espaço.", "collecting_data": "Coletando dados. Ciclos ainda necessários para que o ajuste fino possa começar: {need} ({current}/{min}).", "compare_overlay_profiles": "Sobrepor perfis (fraco)", @@ -853,7 +872,7 @@ "preferences_admin": "Preferências, painel e controle de acesso", "preferences_adv": "Preferências", "prefs_personal": "Aplicam-se apenas à sua conta do Home Assistant.", - "process_history_hint": "Reexecutar correspondência em todos os ciclos armazenados, atualizar sugestões de ajuste, retreinar os modelos ML (se habilitado) e recalcular a saúde dos ciclos. Execute após um lote de revisões.", + "process_history_hint": "Reexecutar correspondência em todos os ciclos armazenados, atualizar sugestões de ajuste, retreinar os modelos ML (se habilitado), recalcular o custo dos ciclos com base nos preços de energia registrados e recalcular a saúde dos ciclos. Execute após um lote de revisões.", "profile_deleted": "Perfil excluído", "profile_poor_health_detail": "Os ciclos atribuídos a este perfil têm formatos inconsistentes ou baixa confiança. Considere reconstruir o envelope ou revisar os ciclos rotulados.", "profiles_intro": "Clique em um perfil para ver estatísticas, fases e limpeza. Agrupe programas quase idênticos (mesmo formato/duração, temperatura ou centrifugação diferentes) para que a correspondência escolha de forma confiável entre eles.", @@ -936,9 +955,10 @@ "advisory_poor_health": "'{name}' tem uma pontuação de ajuste baixa - seus ciclos recentes variam muito ou têm correspondência fraca. Revise seus ciclos ou regrave o perfil para manter a identificação e as estimativas de tempo precisas.", "advisory_shape_drift": "'{name}' desviou-se significativamente da sua curva de potência original. O aparelho pode ter mudado de comportamento com o tempo (por exemplo, calcário, desgaste). Considere regravar este perfil com ciclos recentes.", "advisory_shape_drift_corr": "'{name}' desviou-se significativamente da sua curva de potência original (correlação {corr}). O aparelho pode ter mudado de comportamento com o tempo (por exemplo, calcário, desgaste). Considere regravar este perfil com ciclos recentes.", + "advisory_unmatchable": "'{name}' nunca poderá ter correspondência: não há nenhum ciclo com dados de potência associado a ele, portanto o WashData não tem nada com que comparar um ciclo em andamento. Rotule um ciclo deste programa (ou grave um) para incluí-lo novamente na correspondência.", + "advisory_unmatchable_no_duration": "'{name}' nunca poderá ter correspondência: ele tem um ciclo com dados de potência, mas sem uma duração utilizável, portanto o WashData não tem nenhuma duração com que dimensionar uma correspondência. Grave este programa novamente, ou defina a duração esperada, para incluí-lo novamente na correspondência.", "pg_no_alerts": "Nenhum problema detectado nesta execução.", "pg_override_active": "Usando suas configurações editadas vs. as atuais", - "pg_history_intro": "Reproduza seus ciclos recentes com o detector e o comparador reais. Edite primeiro uma configuração de detecção em Simular para ver aqui uma comparação antes/depois.", "pg_history_empty": "Pressione Executar para reproduzir seus ciclos.", "pg_history_summary": "{detected}/{total} detectados · {correct} correspondidos corretamente", "pg_sweep_no_metric": "Dados insuficientes para pontuar este objetivo.", @@ -1015,7 +1035,11 @@ "hist_scan_failed": "A análise falhou.", "hist_scan_expired": "Essa análise não está mais disponível. Analise novamente.", "hist_import_failed": "A importação falhou.", - "imported_history_readonly": "Detectado em um histórico de potência importado. Influencia a correspondência de programas, mas não é contabilizado nas suas estatísticas e não pode ser recortado nem dividido. Rotule-o para indicar o programa." + "imported_history_readonly": "Detectado em um histórico de potência importado. Influencia a correspondência de programas, mas não é contabilizado nas suas estatísticas e não pode ser recortado nem dividido. Rotule-o para indicar o programa.", + "odometer_intro": "Este total só aumenta. Excluir o registro de um ciclo não o altera e ele continua contando além do limite do histórico armazenado, para que os lembretes de manutenção permaneçam corretos.", + "odometer_correct_hint": "Defina este valor se o WashData registrou um ciclo que nunca aconteceu, ou se o eletrodoméstico já havia realizado ciclos antes de o WashData ser instalado.", + "no_reminders_set": "Ainda não há lembretes de manutenção definidos.", + "toast_program_armed": "Programa preparado para o próximo ciclo: {program}" }, "phase_desc": { "anti_crease": "Giros curtos ocasionais após a conclusão para reduzir amassados.", @@ -1113,6 +1137,10 @@ "label": "Liberação em silêncio da secagem passiva", "doc": "Assim que o ciclo ultrapassa a duração esperada, por quanto tempo a lava-louças deve permanecer silenciosa (abaixo do Limite de Parada) antes de o WashData parar de aguardar uma descarga final e encerrar o ciclo. Aumente este valor se a sua máquina tiver uma longa fase de secagem silenciosa antes de uma descarga final tardia que está passando despercebida - uma janela mais ampla permite que a duração aprendida acompanhe a variação sazonal (água de entrada mais fria = ciclos mais longos) em vez de se fixar na média antiga. Ele apenas encurta a espera em relação ao limite interno de 30 minutos para o pico de fim, nunca a prolonga." }, + "anti_crease_finalize_ratio": { + "label": "Proporção de finalização antiamasso", + "doc": "Até que ponto da duração esperada do programa correspondente um ciclo precisa ter chegado antes de o WashData poder concluir que o giro lento de baixa potência que está vendo é a cauda antiamasso e encerrar o ciclo ali. A duração esperada é a média do programa, portanto, em uma secadora cujo tempo de secagem por sensor acompanha a carga, a maioria dos ciclos nunca atinge 98% dessa média e a cauda fica esperando até o tempo limite de segurança, muitas vezes de 15 a 40 minutos depois de a máquina ter realmente parado. Reduza este valor (por exemplo, 0,75) em uma secadora assim. Deixe as máquinas de lavar no valor padrão: uma lavadora passa a maior parte do ciclo abaixo da Potência Máxima Antiamasso, de modo que essa fração é a única coisa que separa uma fase silenciosa no meio da lavagem da cauda, e reduzi-la pode cortar uma lavagem pela metade. Deixe vazio para o valor padrão (0,98). Esta configuração é diferente da Proporção de encerramento inteligente, que controla uma verificação de encerramento antecipado separada." + }, "anti_wrinkle_enabled": { "doc": "Reconheça os pulsos curtos de baixa potência que uma secadora emite após a fase de aquecimento principal e mantenha-os anexados ao ciclo finalizado, em vez de lê-los como novos ciclos.", "label": "Ativar Detecção Antiamasso" @@ -1134,7 +1162,7 @@ "label": "Potência Máxima Antiamasso" }, "auto_label_confidence": { - "doc": "Se a pontuação da partida no final do ciclo for igual ou superior a isso, o programa será rotulado automaticamente sem qualquer solicitação de confirmação. Aumente-o para exigir maior certeza antes da rotulagem automática; abaixe-o para automatizar mais. Funciona em conjunto com Learning Confidence abaixo dele.", + "doc": "Se a pontuação de correspondência no final do ciclo for igual ou superior a isso, o programa será rotulado automaticamente sem qualquer solicitação de confirmação. Aumente-o para exigir maior certeza antes da rotulagem automática; abaixe-o para automatizar mais. Funciona em conjunto com a Confiança de Aprendizado abaixo dele.", "label": "Confiança de Rotulagem Automática" }, "auto_maintenance": { @@ -1145,6 +1173,10 @@ "doc": "Ciclos mais curtos que isso são descartados como ciclos fantasmas (testes, abertura da porta para adicionar uma meia).", "label": "Duração Mínima do Ciclo" }, + "curve_preroll_seconds": { + "label": "Pré-início da curva", + "doc": "Quantos segundos de leituras provenientes de tentativas de início interrompidas o WashData pode incorporar no começo da curva de um ciclo. A curva de um ciclo normalmente começa na tentativa de início que enfim dá certo, portanto, em uma máquina que faz algumas tentativas antes de engatar de fato (seleção do programa, travamento da porta, primeiro enchimento), os primeiros minutos de atividade real ficam de fora de todas as curvas. Defina este valor um pouco acima da partida prévia mais longa da sua máquina (300 s são mais do que suficientes na maioria dos casos). 0 desativa o recurso. Fique atento: ativá-lo antecipa o início registrado, de modo que os ciclos registrados antes e depois da mudança ficam com durações diferentes para o mesmo programa; as médias aprendidas vão se desviar até os ciclos mais antigos saírem do histórico." + }, "delay_confirm_seconds": { "doc": "A energia deve permanecer na faixa de espera por esse tempo antes que o aparelho seja tratado como aguardando para iniciar, em vez de funcionar.", "label": "Segundos de Confirmação" @@ -1182,7 +1214,7 @@ "label": "Contagem de Repetições de Fim" }, "energy_price_entity": { - "doc": "Sensor com o preço atual da eletricidade por kWh (por exemplo, uma tarifa dinâmica). Tem precedência sobre o preço estático abaixo. Cada ciclo congela o preço em vigor quando termina.", + "doc": "Sensor com o preço atual da eletricidade por kWh (por exemplo, uma tarifa dinâmica). Precisa ser um preço, não a entidade de potência ou de energia da própria tomada: tem precedência sobre o preço estático abaixo, então um contador de kWh aqui cobra cada ciclo pela leitura do medidor em vez da sua tarifa. Com o Custo Ponderado no Tempo ativado, cada ciclo é cobrado pelo preço em vigor em cada momento em que ele rodou; caso contrário, fica congelado nele o preço em vigor quando terminou.", "label": "Entidade de Preço de Energia" }, "energy_price_static": { @@ -1210,7 +1242,7 @@ "label": "Inverter Gatilho Externo (acionar em DESLIGADO)" }, "learning_confidence": { - "doc": "Se a pontuação de correspondência estiver entre este valor e a Confiança do rótulo automático, uma notificação de feedback solicitará que você verifique o programa identificado. Abaixo desta pontuação, a partida é muito incerta para surgir. Deve ser mantido abaixo da Confiança da Etiqueta Automática.", + "doc": "Se a pontuação de correspondência estiver entre este valor e a Confiança de Rotulagem Automática, uma notificação de feedback solicitará que você verifique o programa identificado. Abaixo desta pontuação, a correspondência é incerta demais para ser exibida. Deve ser mantido abaixo da Confiança de Rotulagem Automática.", "label": "Confiança de Aprendizado" }, "linked_device": { @@ -1278,7 +1310,7 @@ "label": "Disparar Eventos HA para Notificações" }, "notify_icon": { - "doc": "Ícone mdi opcional para a notificação (por exemplo, mdi:máquina de lavar). Deixe em branco para o padrão da plataforma.", + "doc": "O pequeno ícone exibido na notificação (por exemplo, mdi:washing-machine). O Android o desenha na barra de status; o iOS o exibe no lugar do ícone do aplicativo e requer o aplicativo complementar 2026.8 ou mais recente. Deixe em branco para o padrão da plataforma.", "label": "Ícone da Notificação" }, "notify_live_chronometer": { @@ -1378,7 +1410,7 @@ "label": "Limite de Correspondência" }, "profile_unmatch_threshold": { - "doc": "Se uma partida ao vivo no meio do ciclo cair abaixo dessa pontuação, a identificação provisória será apagada. Mantenha-o um pouco abaixo do Limite de Correspondência para que uma breve queda na similaridade não reverta a exibição para incomparável.", + "doc": "Se uma correspondência ao vivo no meio do ciclo cair abaixo dessa pontuação, a identificação provisória será apagada. Mantenha-o um pouco abaixo do Limite de Correspondência para que uma breve queda na similaridade não reverta a exibição para sem correspondência.", "label": "Limite de Não Correspondência" }, "progress_reset_delay": { @@ -1389,10 +1421,6 @@ "doc": "Segundos, uma bomba pode funcionar continuamente antes de ser sinalizada como possivelmente travada (dispara o evento de bomba travada).", "label": "Duração da Bomba Travada" }, - "running_dead_zone": { - "doc": "Após o início de um ciclo, as quedas de energia nesta janela são ignoradas. As máquinas de lavar enchem com água fria (caindo perto de 0 W antes do aquecimento) - sem esta proteção, a fase de enchimento parece um fim de ciclo. Isto NÃO ignora dados: o traço de potência total é registrado a partir de T=0. O mecanismo de sugestão mede o padrão de inicialização real da sua máquina e o dimensiona automaticamente.", - "label": "Zona Morta em Execução" - }, "sampling_interval": { "doc": "Tempo esperado entre as leituras do sensor - usado para dimensionar a janela de suavização e iniciar o debounce corretamente. Cada atualização do sensor é capturada independentemente deste valor; ele apenas calibra os cálculos posteriores. O mecanismo de sugestão mede a cadência real do seu sensor em ciclos anteriores e define isso automaticamente.", "label": "Intervalo de Amostragem" @@ -1410,7 +1438,7 @@ "label": "Janela de Suavização" }, "start_duration_threshold": { - "doc": "A energia deve permanecer acima do limite de partida por tanto tempo para confirmar uma partida real, evitando que interruptores liga/desliga em frações de segundo iniciem um ciclo.", + "doc": "A potência deve permanecer acima do Limite de Início por este tempo para confirmar uma partida real, evitando que acionamentos liga/desliga em frações de segundo iniciem um ciclo.", "label": "Duração de Início" }, "start_energy_threshold": { @@ -1418,7 +1446,7 @@ "label": "Energia de Início" }, "start_threshold_w": { - "doc": "A potência deve subir acima deste nível para confirmar que um ciclo foi iniciado. Definir um valor muito baixo causa falsas partidas na energia de espera; programas muito altos e de início lento (enchimento a frio) são perdidos. O mecanismo de sugestão define isso logo acima da potência ativa mais baixa observada pela máquina.", + "doc": "A potência deve subir acima deste nível para confirmar que um ciclo foi iniciado. Defini-lo baixo demais causa falsas partidas a partir da energia em espera; defini-lo alto demais faz perder os programas de início lento (enchimento a frio). O mecanismo de sugestão define isso logo acima da potência ativa mais baixa observada na máquina.", "label": "Limite de Início" }, "stop_threshold_w": { @@ -1520,13 +1548,17 @@ "label": "Intervalo do Perfil de Potência", "doc": "Tamanho do intervalo para o atributo de sensor power_profile por perfil (a matriz plana de watts médios por slot consumida por planejadores externos como EMHASS e tibber_prices). Intervalos menores preservam picos curtos de potência com mais nitidez; intervalos maiores suavizam o perfil. Padrão: 15 min. Apenas em tempo de leitura; não afeta a detecção." }, + "notify_live_silent": { + "label": "Atualizações Silenciosas ao Vivo", + "doc": "Apenas iOS. Atualiza o progresso ao vivo em silêncio: cada atualização chega sem som nem vibração, com uma prioridade mais baixa que economiza bateria e que o iOS pode agrupar por alguns instantes. A primeira atualização de um ciclo (a que inicia a Atividade ao Vivo) e as notificações de início e de término continuam avisando normalmente. Desative esta opção para ser avisado a cada atualização." + }, "notify_live_sticky": { "label": "Manter Notificação ao Vivo ao Toque", "doc": "Apenas Android. Torna a notificação de progresso ao vivo persistente (fixada) para que tocá-la não dispense o encadeamento em andamento. Desativado mantém o comportamento padrão, em que um toque a dispensa." }, "notify_live_click_action": { - "label": "Destino de Toque da Notificação ao Vivo", - "doc": "Apenas Android. Para onde um toque na notificação de progresso ao vivo abre (por exemplo, /lovelace/lavanderia, ou uma URL completa) em vez da página inicial do aplicativo. Deixe em branco para o padrão." + "label": "Destino de Toque da Notificação", + "doc": "Para onde um toque em uma notificação deste eletrodoméstico abre, no Android e no iOS. Em branco abre o painel do WashData neste eletrodoméstico. Informe o caminho de um painel (por exemplo, /lovelace/laundry) ou uma URL completa para abrir outro destino, ou \"none\" para nenhum destino de toque." }, "door_opens_at_end": { "label": "Porta Abre Automaticamente ao Final", @@ -1539,6 +1571,10 @@ "profile_evidence_sources": { "label": "Ciclos que moldam um programa", "doc": "Quais ciclos são usados para construir a curva de potência de cada programa e para comparar um ciclo finalizado com ela. Ao desmarcar um tipo, ele para de moldar seus programas sem que nada seja excluído - os ciclos continuam na sua lista de Ciclos e ainda podem ser rotulados ou removidos. Útil se você não confia nos dados importados. As estatísticas não são afetadas: elas sempre contabilizam apenas os ciclos que esta máquina realmente executou. Desmarcar tudo é ignorado, pois um programa sem ciclos por trás nunca conseguiria corresponder." + }, + "energy_price_dynamic": { + "label": "Custo Ponderado no Tempo", + "doc": "Cobra cada ciclo pelo preço em vigor em cada momento em que ele rodou, em vez do preço único do instante em que terminou. Só vale para uma entidade de preço (um preço estático não muda). Não exige configuração extra: o preço é acompanhado ao vivo enquanto o ciclo roda, e Processar histórico, em Diagnósticos, recalcula o custo dos ciclos antigos a partir do recorder. Desativado = o comportamento clássico, um preço congelado no final." } }, "setting_group": { @@ -1650,7 +1686,8 @@ "splitting": "Dividindo…", "deleting": "Excluindo…", "imported": "Importado", - "preparing": "Preparando…" + "preparing": "Preparando…", + "unlabelling": "Removendo rótulo…" }, "tab": { "advanced": "Avançado", @@ -1734,6 +1771,7 @@ "recording_stopped": "Gravação parada", "review_saved": "Revisão salva", "settings_conflicts": "Corrija os conflitos de configurações destacados antes de salvar.", + "settings_no_changes": "Nenhuma alteração para salvar", "settings_reverted": "Configurações revertidas; a integração está recarregando", "settings_saved": "Configurações salvas; a integração está recarregando", "suggestions_applied": "Sugestões aplicadas; a integração está recarregando", @@ -1788,7 +1826,11 @@ "export_selective_done": "Exportação baixada", "import_selective_done": "Importados {profiles} perfil(is) e {cycles} ciclo(s)", "hist_csv_required": "Carregue primeiro um arquivo CSV ou cole o conteúdo dele", - "file_read_failed": "Não foi possível ler esse arquivo" + "file_read_failed": "Não foi possível ler esse arquivo", + "unlabel_done": "Rótulo removido de {count} ciclo(s)", + "unlabel_failed": "Falha ao remover o rótulo: {error}", + "odometer_saved": "Total de ciclos salvo", + "odometer_save_failed": "Não foi possível salvar o total: {error}" }, "suggestion": { "both_agree": "WashData recomenda", @@ -1835,10 +1877,6 @@ "higher": "Confirma apenas correspondências de alta confiança; mais ciclos ficam sem rótulo", "lower": "Corresponde a mais ciclos; alguns podem estar ligeiramente errados" }, - "running_dead_zone": { - "higher": "Ignora breves picos de potência em repouso – menos sensível ao ruído", - "lower": "Responde a impulsos mais curtos – detecta atividade transitória rápida" - }, "start_threshold_w": { "higher": "Evita partidas falsas por breves picos de potência", "lower": "Detecta programas de baixa potência; pode ser ativado por ruído" @@ -1863,6 +1901,8 @@ "end_repeat_count": "{false}/{total} ciclos limpos ({pct}%) tiveram uma pausa interna de >60s que foi retomada; exigir {val} confirmação(ões) de fim.", "learning_confidence": "Confiança p05 de {cycles} ciclos rotulados pelo usuário ({p05}); abaixo disso, solicitar verificação.", "match_interval": "Com base na cadência de atualização observada (mediana={median}s) * 10.", + "match_interval_capped": "Limitado para que {persistence} correspondências consecutivas caibam em {pct}% do programa mais curto ({shortest}s); apenas a cadência de atualização (mediana={median}s) teria sugerido um intervalo maior.", + "match_interval_floored": "Sozinho, o programa mais curto ({shortest}s) limitaria este valor a {cap}s, abaixo do mínimo de {minimum}s, por isso ele fica nesse mínimo: {persistence} correspondências consecutivas levam {budget}s, ou seja, mais de {pct}% desse programa. A correspondência só é executada quando chega uma leitura (mediana={median}s), portanto o mínimo não custa nada.", "max_duration_ratio": "Com base nas durações de ciclos rotulados (p95={p95}).", "min_active": "Com base na potência ativa mínima ({min}W) observada no último ciclo.", "min_duration_ratio": "Mantido o mais baixo possível para que um programa seja reconhecido logo no início do ciclo; os limiares de confiança e de ambiguidade evitam confirmações prematuras.", @@ -1917,6 +1957,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Lava-louças: segundos de silêncio após a duração esperada antes de liberar a espera da descarga de fim de ciclo", "smart_termination_duration_ratio": "Fração da duração esperada do programa correspondente que um ciclo deve atingir antes de o Encerramento inteligente poder encerrá-lo mais cedo; reduza-a para máquinas que dependem da carga ou da temperatura", + "anti_crease_finalize_ratio": "Fração da duração esperada do programa correspondente que um ciclo deve atingir antes de a cauda de giro antiamasso poder ser finalizada; reduza-a em secadoras com uma cauda de giro longa, deixe as máquinas de lavar no valor padrão", "anti_wrinkle_enabled": "Absorver os pulsos de giro após a fase principal em vez de lê-los como novos ciclos", "anti_wrinkle_exit_power": "A potência deve cair abaixo deste valor entre os pulsos para que o modo antiamasso permaneça ativo", "anti_wrinkle_idle_timeout": "Tempo de silêncio permitido entre dois pulsos de giro antes de o modo antiamasso terminar", @@ -1942,7 +1983,8 @@ "dtw_ddtw_scale": "Etapa 3: distância de meia saturação DDTW; menor = mais sensível à forma (padrão 30)", "dtw_refine_top_n": "Etapa 3: candidatos reavaliados pelo DTW; aumente para 7-9 se o perfil correto se classificar em 4.º-5.º (padrão 5)", "duration_scale": "Etapa 4: log-razão em que a concordância de duração é reduzida à metade; menor = penalidade mais severa (padrão 0,175)", - "energy_scale": "Etapa 4: log-razão em que a concordância de energia é reduzida à metade; menor = penalidade mais severa (padrão 0,25)" + "energy_scale": "Etapa 4: log-razão em que a concordância de energia é reduzida à metade; menor = penalidade mais severa (padrão 0,25)", + "curve_preroll_seconds": "Segundos de leituras de tentativas de início interrompidas incorporados no começo da curva de um ciclo; 0 = desativado" }, "col": { "profile_tip": "Nome do programa correspondente. Sem rótulo significa que nenhum perfil correspondeu no fim do ciclo.", @@ -1950,10 +1992,11 @@ "date_tip": "Data e hora de início do ciclo.", "duration_tip": "Tempo total de execução do ciclo, do início ao fim.", "energy_tip": "Energia total consumida (kWh). Calculada integrando a potência ao longo do tempo.", - "cost_tip": "Custo de energia deste ciclo, congelado na conclusão usando o preço em vigor naquele momento (energia x preço por kWh). Defina um preço nas Configurações para preenchê-lo.", + "cost_tip": "Custo de energia deste ciclo, congelado na conclusão. Com o Custo Ponderado no Tempo ativado, a curva de potência é cobrada pelo preço em vigor em cada momento; caso contrário, energia x o preço único em vigor no final. Defina um preço nas Configurações para preenchê-lo.", "confidence_tip": "Confiança de correspondência do perfil (0-100%). Quão próxima a curva de potência do ciclo correspondeu ao programa identificado.", "health_tip": "Saúde do ciclo por ML (mais alto = melhor). Clique em um ciclo para inspecioná-lo e revisá-lo.", - "flags_tip": "Sinalizadores de revisão, anomalia e origem do ciclo. Passe o cursor sobre um ícone para ver os detalhes." + "flags_tip": "Sinalizadores de revisão, anomalia e origem do ciclo. Passe o cursor sobre um ícone para ver os detalhes.", + "cost_dynamic_tip": "Ponderado no tempo: cobrado pelo preço em vigor durante o ciclo (preço efetivo {price} {cur}/kWh)." }, "stat": { "avg": "méd.", @@ -1980,9 +2023,9 @@ "finished": "O ciclo atingiu um estado final e foi encerrado." }, "store": { + "search_ph2": "Pesquisar por marca ou modelo…", "your_model_tip": "Este é o eletrodoméstico que você declarou em Configurações", "your_model": "Seu", - "search_brand_ph": "Pesquisar por marca…", "programs_count": "Programas: {n}", "browse": "Navegar", "device": "Dispositivo", @@ -2027,7 +2070,8 @@ "golden": "Reprocessamento: marcação de ciclos de referência", "suggestions": "Reprocessamento: sugestões", "ml_training": "Reprocessamento: treinamento de ML", - "health": "Reprocessamento: saúde dos ciclos" + "health": "Reprocessamento: saúde dos ciclos", + "costs": "Reprocessamento: custos de energia" }, "pg_sweep": { "optimize": "Otimizar: {param}" diff --git a/custom_components/ha_washdata/translations/panel/pt.json b/custom_components/ha_washdata/translations/panel/pt.json index ee2cf703..451fdbd2 100644 --- a/custom_components/ha_washdata/translations/panel/pt.json +++ b/custom_components/ha_washdata/translations/panel/pt.json @@ -1,5 +1,7 @@ { "badge": { + "quiet_tail_tip": "Perto do fim, este programa fica em silêncio durante cerca de {mins} min e depois consome cerca de {watts} W durante {secs} s antes de terminar. Observado em {seen} de {measured} ciclos medidos: o aparelho não o faz em todas as utilizações.", + "quiet_tail": "silêncio final ~{mins} min", "artifact_tip": "{n} anomalias detectadas (por exemplo, porta aberta a meio do ciclo) – abra para as ver no gráfico", "auto": "(detectado automaticamente)", "built_in_tag": "incorporado", @@ -29,11 +31,14 @@ "approved": "Aprovado", "awaiting": "A aguardar aprovação", "imported_tip": "Importado da loja da comunidade. Usado apenas para correspondência, não contabilizado nas estatísticas.", + "unmatchable": "correspondência impossível", "not_importable": "n/d aqui", "exists": "já existe", - "backfilled_tip": "Detetado num histórico de potência importado. Influencia apenas a correspondência de programas, não é contabilizado nas estatísticas." + "backfilled_tip": "Detetado num histórico de potência importado. Influencia apenas a correspondência de programas, não é contabilizado nas estatísticas.", + "armed": "(aplica-se ao próximo ciclo)" }, "btn": { + "reset_zoom": "Repor zoom", "set_brand_model": "Definir marca e modelo", "refresh_catalog": "Actualizar catálogo", "add_device": "+ Adicionar dispositivo", @@ -167,6 +172,8 @@ "trim": "Cortar", "trim_split": "Cortar / Dividir", "undo": "Anular", + "unlabel_selected": "Remover rótulo dos selecionados ({n})", + "unlabel_selected_tip": "Remove o rótulo de perfil dos ciclos selecionados. Continuam no seu histórico e poderão ser rotulados novamente mais tarde; deixam apenas de influenciar este perfil.", "use": "Usar", "wipe_all": "Apagar todos os dados", "delete_selected": "Eliminar selecionados ({n})", @@ -174,7 +181,6 @@ "pg_apply_best": "Aplicar melhor", "pg_apply_to_settings": "Guardar nas definições", "pg_apply_to_settings_tip": "Copiar os valores que editou aqui para as definições ativas deste dispositivo", - "search": "Pesquisar", "import": "Importar", "disconnect": "Desconectar", "connect_github": "Conectar GitHub", @@ -201,7 +207,8 @@ "hist_read_recorder": "Ler do Home Assistant", "hist_scan": "Procurar ciclos", "hist_import_n": "Importar {n} ciclos", - "hist_goto_cycles": "Ver os ciclos" + "hist_goto_cycles": "Ver os ciclos", + "save_total_cycles": "Guardar total" }, "conflict": { "anti_wrinkle_exit": { @@ -214,6 +221,12 @@ }, "attn_sub": "Corrija os conflitos antes de guardar", "attn_title": "Conflitos de definições: {n}", + "price_entity": { + "power_sensor": "Este é o Sensor de Potência do dispositivo, não um preço. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo, pelo que os ciclos seriam cobrados a watts por kWh - limpe este campo.", + "energy_sensor": "Esta é a Entidade do Contador de Energia do dispositivo, não um preço. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo, pelo que os ciclos seriam cobrados à leitura do contador por kWh - limpe este campo.", + "device_class": "Este sensor mede {dc}, não um preço por kWh. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo - limpe este campo ou aponte-o para um sensor de tarifa.", + "unit": "Este sensor indica valores em {unit}, o que não é um preço por kWh. Uma entidade de preço tem precedência sobre o Preço Estático de Energia abaixo - limpe este campo ou aponte-o para um sensor de tarifa." + }, "settings_banner": "Conflitos de definições: {n}. Verifique as secções realçadas e corrija-os antes de guardar.", "settings_banner_btn": "Ir para o primeiro", "confidence": { @@ -301,7 +314,7 @@ "pg_settings_source": "Origem das definições", "phase_catalog": "Catálogo de fases", "playground": "Área de testes", - "process_history": "Histórico de processamento", + "process_history": "Processar histórico", "settings_history": "Histórico de definições", "status": "Estado", "status_graph": "Gráfico de estado", @@ -312,12 +325,12 @@ "wipe_history": "Apagar histórico", "recommendations": "Recomendações ({n})", "pg_detection_params": "Definições de deteção", - "pg_timeline": "Cronologia de eventos", "pg_outcome": "Resultado da simulação", "pg_across_cycles": "Em todos os seus ciclos", "community_store": "Loja da comunidade", "online_account": "Loja da comunidade e funcionalidades online", - "import_power_history": "Importar histórico de potência" + "import_power_history": "Importar histórico de potência", + "service_status": "Estado da manutenção" }, "health": { "fair": "Qualidade de perfil aceitável", @@ -325,6 +338,10 @@ "poor": "⚠ Qualidade de perfil fraca" }, "lbl": { + "envelope_position_tip": "Posição na curva gravada do próprio programa correspondente, {pct}%. É medida alinhando este ciclo com o perfil em vez de contar o tempo, pelo que se mantém correta quando um ciclo é mais longo ou mais curto do que o habitual. É atualizada enquanto o aparelho está em silêncio, pelo que pode ficar atrasada durante uma fase ativa.", + "envelope_position": "curva {pct}%", + "aria_playground_chart3": "Gráfico interativo da potência do ciclo: passe o cursor para ler tempo/potência, percorra ou use o gesto de pinça para ampliar, arraste para deslocar", + "device_unresolved": "Dispositivo indisponível ({id})", "drag_to_resize": "Arraste para redimensionar", "actions": "Acções", "activity": "Actividade", @@ -460,7 +477,7 @@ "profile_index": "índice do perfil →", "profile_name": "Nome do perfil", "program": "Programa", - "program_tip": "Substitua o perfil correspondido ao ciclo atual. A deteção automática permite que a integração escolha a melhor correspondência automaticamente. Fixe um programa específico para forçar a correspondência quando a deteção automática está errada ou quando sabe o que está em execução.", + "program_tip": "Substitua o perfil correspondido ao ciclo atual. A deteção automática permite que a integração escolha a melhor correspondência automaticamente. Fixe um programa específico para forçar a correspondência quando a deteção automática está errada ou quando sabe o que está em execução. Escolha um antes de ligar o aparelho e será aplicado assim que o próximo ciclo começar.", "progress": "Progresso", "raw_socket": "Socket bruto", "read_only": "Só de leitura", @@ -495,6 +512,7 @@ "stage4": "Etapa 4 – concordância", "status": "Estado", "tail_trim": "Corte final (s)", + "tap_to_dismiss": "toque no gráfico para fechar", "timer_auto_pause": "Pausa automática", "timer_min": "min", "timer_msg_placeholder": "Mensagem (opcional, {device}/{program}/{minutes})", @@ -531,9 +549,7 @@ "pg_time_left_model": "Tempo restante (modelo)", "pg_time_left_model_tip": "Tempo restante estimado pelo modelo (estimador de fase + combinação de ML), não uma contagem decrescente fixa", "phase": "Fase", - "pg_mode_simulate": "Simular", "pg_mode_history": "Testar no histórico", - "pg_mode_sweep": "Varrimento", "pg_ev_detected": "Detetado", "pg_ev_match": "Correspondência confirmada", "pg_ev_match_changed": "Correspondência alterada", @@ -572,14 +588,11 @@ "pg_stress_idle_w_desc": "Substitui o piso de repouso detectado automaticamente (deixe em branco para automático).", "pg_stress_toggle": "Testar terminação em repouso", "pg_stress_toggle_desc": "Simula o electrodoméstico mantido no seu consumo em repouso após o fim da gravação – mostra se e quando o WashData para o ciclo.", - "pg_sweep_2d": "Mapa de calor 2D", - "pg_sweep_param_y": "2.º parâmetro", "pg_obj_match": "Precisão de correspondência", "pg_obj_endtiming": "Precisão do momento de fim", "pg_obj_falseend": "Taxa de fins falsos", "pg_obj_overrun": "Desvio de duração", "pg_obj_ambiguity": "Taxa de ambiguidade", - "pg_best_combo": "melhor combinação", "pg_mode_optimize": "Otimizar", "pg_group_matching": "Correspondência de programa", "pg_zoom_hint": "faça duplo clique para repor o zoom", @@ -658,7 +671,12 @@ "task_history_import_apply": "A importar ciclos", "evidence_real_cycles": "Ciclos que esta máquina executou", "evidence_reference_cycles": "Transferidos da loja da comunidade", - "evidence_backfill_cycles": "Encontrados num histórico de potência importado" + "evidence_backfill_cycles": "Encontrados num histórico de potência importado", + "zoom_hint_touch": "gesto de pinça para ampliar", + "zoom_hint_touch_zoomed": "gesto de pinça para ampliar · duplo toque para repor", + "cycles_since_service": "{since} / {total} ciclos", + "total_cycles_run": "ciclos realizados no total", + "correct_total_cycles": "Corrigir o total" }, "log": { "all_levels": "Todos os níveis", @@ -708,7 +726,7 @@ "merge_cycles": "Combinar {n} ciclos", "new_group": "Novo grupo de perfis", "new_phase": "Nova fase", - "process_history_msg": "Reexecute a correspondência, atualize as sugestões, retreine os modelos ML (se ativados) e recalcule o estado de todos os ciclos armazenados. Isto pode demorar algum tempo.", + "process_history_msg": "Reexecute a correspondência, atualize as sugestões, retreine os modelos ML (se ativados), recalcule o custo dos ciclos com base nos preços de energia registados e recalcule o estado de todos os ciclos armazenados. Isto pode demorar algum tempo.", "process_history_ok": "Processar", "process_history_title": "Processar histórico", "process_recording": "Processar gravação", @@ -741,6 +759,7 @@ "history_import": "Importar histórico de potência" }, "msg": { + "pg_canvas_empty3": "Escolha um ciclo acima e prima Executar para o simular. Depois passe o cursor para ler os valores, percorra ou use o gesto de pinça para ampliar e arraste para deslocar.", "tail_trim_hint": "Número de segundos a remover do fim", "store_sibling_hint": "Não há nada partilhado para o seu modelo exato? Um modelo muito semelhante da mesma marca é normalmente um bom ponto de partida.", "store_declare_appliance": "Indique ao WashData qual o electrodoméstico que possui e este separador mostrará as configurações que outras pessoas partilharam para ele. Também pode escrever uma marca acima para explorar.", @@ -754,7 +773,7 @@ "artifact_spike_detail": "Ultrapassou a banda de potência habitual durante ~{n}s.", "auto_label_intro": "Atribua perfis a ciclos sem rótulo cuja confiança de correspondência ultrapasse o limiar.", "automations_intro": "O WashData dispara os eventos {start} / {end} e expõe entidades, por isso as notificações e ações são melhor criadas como automatizações normais do Home Assistant. As automatizações que utilizam este dispositivo aparecem abaixo.", - "cleanup_intro": "Todos os ciclos rotulados sobrepostos. Assinale os valores aberrantes e elimine para limpar o perfil.", + "cleanup_intro": "Todos os ciclos rotulados sobrepostos. Assinale os valores aberrantes e depois remova-lhes o rótulo (mantêm-se no histórico, mas deixam de influenciar este perfil) ou elimine-os por completo.", "clear_debug_hint": "Remova os dados de depuração armazenados para libertar espaço.", "collecting_data": "A recolher dados. Ciclos ainda necessários para o ajuste fino poder começar: {need} ({current}/{min}).", "compare_overlay_profiles": "Sobrepor perfis (ténue)", @@ -853,7 +872,7 @@ "preferences_admin": "Preferências, painel e controlo de acesso", "preferences_adv": "Preferências", "prefs_personal": "Aplicam-se apenas à sua conta do Home Assistant.", - "process_history_hint": "Reexecute a correspondência em todos os ciclos armazenados, atualize as sugestões de ajuste, retreine os modelos ML (se ativados) e recalcule o estado dos ciclos. Execute após um lote de revisões.", + "process_history_hint": "Reexecute a correspondência em todos os ciclos armazenados, atualize as sugestões de ajuste, retreine os modelos ML (se ativados), recalcule o custo dos ciclos com base nos preços de energia registados e recalcule o estado dos ciclos. Execute após um lote de revisões.", "profile_deleted": "Perfil eliminado", "profile_poor_health_detail": "Os ciclos atribuídos a este perfil têm formas inconsistentes ou baixa confiança. Considere reconstruir o envelope ou rever os ciclos rotulados.", "profiles_intro": "Clique num perfil para ver estatísticas, fases e limpeza. Agrupe programas quase idênticos (mesma forma/duração, temperatura ou centrifugação diferentes) para que a correspondência escolha de forma fiável entre eles.", @@ -936,9 +955,10 @@ "advisory_poor_health": "'{name}' tem uma pontuação de correspondência baixa - os ciclos recentes variam muito ou correspondem fracamente. Reveja os ciclos ou volte a gravar o perfil para que o reconhecimento e as estimativas de tempo se mantenham precisos.", "advisory_shape_drift": "'{name}' afastou-se significativamente da sua curva de potência original. O aparelho pode ter alterado o comportamento ao longo do tempo (por ex. calcário, desgaste). Considere voltar a gravar este perfil com ciclos recentes.", "advisory_shape_drift_corr": "'{name}' afastou-se significativamente da sua curva de potência original (correlação {corr}). O aparelho pode ter alterado o comportamento ao longo do tempo (por ex. calcário, desgaste). Considere voltar a gravar este perfil com ciclos recentes.", + "advisory_unmatchable": "'{name}' nunca poderá ser correspondido: não tem nenhum ciclo com dados de potência associado, pelo que o WashData não tem nada com que comparar um ciclo em curso. Rotule um ciclo deste programa (ou grave um) para o voltar a incluir na correspondência.", + "advisory_unmatchable_no_duration": "'{name}' nunca poderá ser correspondido: tem um ciclo com dados de potência mas sem uma duração utilizável, pelo que o WashData não tem qualquer duração com que dimensionar uma correspondência. Volte a gravar este programa, ou defina a duração prevista, para o voltar a incluir na correspondência.", "pg_no_alerts": "Nenhum problema detetado nesta execução.", "pg_override_active": "A usar as suas definições editadas vs. as atuais", - "pg_history_intro": "Reproduza os seus ciclos recentes com o detetor e o comparador reais. Edite primeiro uma definição de deteção em Simular para ver aqui uma comparação antes/depois.", "pg_history_empty": "Prima Executar para reproduzir os seus ciclos.", "pg_history_summary": "{detected}/{total} detetados · {correct} reconhecidos corretamente", "pg_sweep_no_metric": "Dados insuficientes para pontuar este objetivo.", @@ -1015,11 +1035,16 @@ "hist_scan_failed": "A análise falhou.", "hist_scan_expired": "Essa análise já não está disponível. Analise novamente.", "hist_import_failed": "A importação falhou.", - "imported_history_readonly": "Detetado num histórico de potência importado. Influencia a correspondência de programas, mas não é contabilizado nas suas estatísticas e não pode ser cortado nem dividido. Rotule-o para indicar o programa." + "imported_history_readonly": "Detetado num histórico de potência importado. Influencia a correspondência de programas, mas não é contabilizado nas suas estatísticas e não pode ser cortado nem dividido. Rotule-o para indicar o programa.", + "odometer_intro": "Este total só aumenta. Eliminar o registo de um ciclo não o altera e continua a contar além do limite do histórico guardado, para que os lembretes de manutenção se mantenham corretos.", + "odometer_correct_hint": "Defina este valor se o WashData registou um ciclo que nunca aconteceu, ou se o eletrodoméstico já tinha realizado ciclos antes de o WashData ser instalado.", + "no_reminders_set": "Ainda não há lembretes de manutenção definidos.", + "toast_program_armed": "Programa preparado para o próximo ciclo: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Máquina de lavar louça: segundos de silêncio após a duração esperada antes de libertar a espera da descarga de fim de ciclo", "smart_termination_duration_ratio": "Fração da duração esperada do programa correspondente que um ciclo deve atingir antes de a Terminação inteligente o poder terminar mais cedo; reduza-a para máquinas que dependem da carga ou da temperatura", + "anti_crease_finalize_ratio": "Fração da duração esperada do programa correspondente que um ciclo deve atingir antes de a cauda de rotação anti-vincos poder ser finalizada; reduza-a em máquinas de secar com uma cauda de rotação longa, deixe as máquinas de lavar no valor predefinido", "anti_wrinkle_enabled": "Absorver os pulsos de rotação após a fase principal em vez de os ler como novos ciclos", "anti_wrinkle_exit_power": "A potência deve descer abaixo deste valor entre pulsos para que o modo anti-vincos permaneça ativo", "anti_wrinkle_idle_timeout": "Tempo de silêncio permitido entre dois pulsos de rotação antes de o modo anti-vincos terminar", @@ -1039,13 +1064,14 @@ "energy_weight": "Fase 4: o quanto a concordância de energia afeta a pontuação final (predefinição 0,22)", "profile_match_min_duration_ratio": "Fase 1: duração mínima do ciclo como fração da duração do perfil; predefinição 0,1 (10%)", "profile_match_max_duration_ratio": "Fase 1: duração máxima do ciclo como fração da duração do perfil; predefinição 1,5 (150%)", - "keep_min_score": "Fase 2: pontuação mínima para permanecer em jogo; a predefinição 0,1 admite candidatos fracos, as fases seguintes escolhem o melhor", + "keep_min_score": "Fase 2: pontuação mínima para permanecer na disputa; a predefinição 0,1 admite correspondências fracas, as fases seguintes escolhem a melhor", "dtw_blend": "Fase 3: 0 = apenas pontuação central, 1 = apenas pontuação DTW, 0,5 = mistura equilibrada (predefinição)", "dtw_ensemble_w": "Fase 3 (modo ensemble): peso em L1 escalado vs DTW derivativo; a predefinição 0,7 favorece a sensibilidade ao nível", "dtw_ddtw_scale": "Fase 3: distância de semissaturação DDTW; menor = maior sensibilidade à forma (predefinição 30)", "dtw_refine_top_n": "Fase 3: candidatos reavaliados pelo DTW; aumente para 7-9 se o perfil correto se classificar em 4.º-5.º (predefinição 5)", "duration_scale": "Fase 4: log-razão em que a concordância de duração é reduzida a metade; menor = penalidade mais severa (predefinição 0,175)", - "energy_scale": "Fase 4: log-razão em que a concordância de energia é reduzida a metade; menor = penalidade mais severa (predefinição 0,25)" + "energy_scale": "Fase 4: log-razão em que a concordância de energia é reduzida a metade; menor = penalidade mais severa (predefinição 0,25)", + "curve_preroll_seconds": "Segundos de leituras de tentativas de arranque abortadas incorporados no início da curva de um ciclo; 0 = desativado" }, "phase_desc": { "anti_crease": "Rotações curtas ocasionais após a conclusão para reduzir vincos.", @@ -1143,6 +1169,10 @@ "label": "Libertação em silêncio da secagem passiva", "doc": "Assim que o ciclo ultrapassa a sua duração esperada, durante quanto tempo a máquina de lavar louça deve permanecer silenciosa (abaixo do Limiar de Paragem) antes de o WashData deixar de aguardar uma descarga final e terminar o ciclo. Aumente este valor se a sua máquina tiver uma longa fase de secagem silenciosa antes de uma descarga final tardia que está a ser ignorada - uma janela mais ampla permite que a duração aprendida acompanhe a variação sazonal (água de entrada mais fria = ciclos mais longos) em vez de se fixar na média antiga. Apenas encurta a espera face ao limite interno de 30 minutos para o pico de fim, nunca a prolonga." }, + "anti_crease_finalize_ratio": { + "label": "Rácio de finalização anti-vincos", + "doc": "Até que ponto da duração esperada do programa correspondente um ciclo tem de ter chegado antes de o WashData poder concluir que a rotação lenta de baixa potência que está a observar é a cauda anti-vincos e terminar aí o ciclo. A duração esperada é a média do programa, pelo que numa máquina de secar cujo tempo de secagem por sonda acompanha a carga a maioria dos ciclos nunca atinge 98% dessa média e a cauda fica à espera até ao tempo limite de segurança, muitas vezes 15 a 40 minutos depois de a máquina ter realmente parado. Reduza este valor (por exemplo, 0,75) numa máquina de secar assim. Deixe as máquinas de lavar roupa no valor predefinido: uma máquina de lavar passa a maior parte do ciclo abaixo da Potência Máxima Anti-Vincos, pelo que esta fração é a única coisa que separa uma fase silenciosa a meio da lavagem da cauda, e baixá-la pode cortar uma lavagem a meio. Deixe vazio para o valor predefinido (0,98). Esta definição é distinta do Rácio de terminação inteligente, que condiciona uma verificação de fim antecipado separada." + }, "anti_wrinkle_enabled": { "doc": "Reconheça os pulsos curtos de baixa potência que uma secadora emite após a fase de aquecimento principal e mantenha-os anexados ao ciclo finalizado, em vez de lê-los como novos ciclos.", "label": "Activar Detecção Anti-Vincos" @@ -1164,7 +1194,7 @@ "label": "Potência Máxima Anti-Vincos" }, "auto_label_confidence": { - "doc": "Se a pontuação da partida no final do ciclo for igual ou superior a isso, o programa será rotulado automaticamente sem qualquer solicitação de confirmação. Aumente-o para exigir maior certeza antes da rotulagem automática; abaixe-o para automatizar mais. Funciona em conjunto com Learning Confidence abaixo dele.", + "doc": "Se a pontuação de correspondência no final do ciclo for igual ou superior a este valor, o programa é rotulado automaticamente sem qualquer pedido de confirmação. Aumente-o para exigir maior certeza antes da rotulagem automática; baixe-o para automatizar mais. Funciona em conjunto com a Confiança de Aprendizagem apresentada abaixo.", "label": "Confiança de Rotulagem Automática" }, "auto_maintenance": { @@ -1175,6 +1205,10 @@ "doc": "Ciclos mais curtos que isso são descartados como ciclos fantasmas (testes, abertura da porta para adicionar uma meia).", "label": "Duração Mínima do Ciclo" }, + "curve_preroll_seconds": { + "label": "Pré-arranque da curva", + "doc": "Quantos segundos de leituras provenientes de tentativas de arranque abortadas o WashData pode incorporar no início da curva de um ciclo. A curva de um ciclo começa normalmente na tentativa de arranque que acaba por ser bem-sucedida, pelo que numa máquina que tenta algumas vezes antes de arrancar mesmo (seleção do programa, bloqueio da porta, primeiro enchimento) faltam os primeiros minutos de atividade real em todas as curvas. Defina este valor um pouco acima do arranque prévio mais longo da sua máquina (300 s chegam e sobram na maioria dos casos). 0 desativa a função. Tenha em atenção que ativá-la antecipa o início registado, pelo que os ciclos registados antes e depois da alteração ficam com durações diferentes para o mesmo programa; as médias aprendidas vão derivar até os ciclos mais antigos saírem do histórico." + }, "delay_confirm_seconds": { "doc": "A energia deve permanecer na faixa de espera por esse tempo antes que o aparelho seja tratado como aguardando para iniciar, em vez de funcionar.", "label": "Segundos de Confirmação" @@ -1212,7 +1246,7 @@ "label": "Contagem de Repetições de Fim" }, "energy_price_entity": { - "doc": "Sensor com o preço atual da eletricidade por kWh (por exemplo, uma tarifa dinâmica). Tem precedência sobre o preço estático abaixo. Cada ciclo congela o preço em vigor quando termina.", + "doc": "Sensor com o preço atual da eletricidade por kWh (por exemplo, uma tarifa dinâmica). Tem de ser um preço, não a própria entidade de potência ou de energia da tomada: tem precedência sobre o preço estático abaixo, pelo que um contador de kWh aqui cobra cada ciclo à leitura do contador em vez da sua tarifa. Com o Custo Ponderado no Tempo ativado, cada ciclo é cobrado ao preço em vigor em cada momento em que decorreu; caso contrário, fica-lhe fixado o preço em vigor quando terminou.", "label": "Entidade de Preço de Energia" }, "energy_price_static": { @@ -1240,7 +1274,7 @@ "label": "Inverter Gatilho Externo (activar em DESLIGADO)" }, "learning_confidence": { - "doc": "Se a pontuação de correspondência estiver entre este valor e a Confiança do rótulo automático, uma notificação de feedback solicitará que você verifique o programa identificado. Abaixo desta pontuação, a partida é muito incerta para surgir. Deve ser mantido abaixo da Confiança da Etiqueta Automática.", + "doc": "Se a pontuação de correspondência estiver entre este valor e a Confiança de Rotulagem Automática, uma notificação de feedback pede-lhe para verificar o programa identificado. Abaixo desta pontuação, a correspondência é demasiado incerta para ser apresentada. Deve ser mantido abaixo da Confiança de Rotulagem Automática.", "label": "Confiança de Aprendizagem" }, "linked_device": { @@ -1308,7 +1342,7 @@ "label": "Disparar Eventos HA para Notificações" }, "notify_icon": { - "doc": "Ícone mdi opcional para a notificação (por exemplo, mdi:máquina de lavar). Deixe em branco para o padrão da plataforma.", + "doc": "O pequeno ícone apresentado na notificação (por exemplo, mdi:washing-machine). O Android desenha-o na barra de estado; o iOS mostra-o em vez do ícone da aplicação e requer a aplicação complementar 2026.8 ou mais recente. Deixe em branco para a predefinição da plataforma.", "label": "Ícone da Notificação" }, "notify_live_chronometer": { @@ -1408,7 +1442,7 @@ "label": "Limiar de Correspondência" }, "profile_unmatch_threshold": { - "doc": "Se uma partida ao vivo no meio do ciclo cair abaixo dessa pontuação, a identificação provisória será apagada. Mantenha-o um pouco abaixo do Limite de Correspondência para que uma breve queda na similaridade não reverta a exibição para incomparável.", + "doc": "Se uma correspondência em direto a meio do ciclo descer abaixo desta pontuação, a identificação provisória é apagada. Mantenha-o um pouco abaixo do Limiar de Correspondência para que uma breve queda na semelhança não faça a apresentação voltar a sem correspondência.", "label": "Limiar de Não Correspondência" }, "progress_reset_delay": { @@ -1419,10 +1453,6 @@ "doc": "Segundos, uma bomba pode funcionar continuamente antes de ser sinalizada como possivelmente travada (dispara o evento de bomba travada).", "label": "Duração de Bomba Presa" }, - "running_dead_zone": { - "doc": "Após o início de um ciclo, as quedas de energia nesta janela são ignoradas. As máquinas de lavar enchem com água fria (caindo perto de 0 W antes do aquecimento) - sem esta proteção, a fase de enchimento parece um fim de ciclo. Isto NÃO ignora dados: o traço de potência total é registrado a partir de T=0. O mecanismo de sugestão mede o padrão de inicialização real da sua máquina e o dimensiona automaticamente.", - "label": "Zona Morta em Execução" - }, "sampling_interval": { "doc": "Tempo esperado entre as leituras do sensor - usado para dimensionar a janela de suavização e iniciar o debounce corretamente. Cada atualização do sensor é capturada independentemente deste valor; ele apenas calibra os cálculos posteriores. O mecanismo de sugestão mede a cadência real do seu sensor em ciclos anteriores e define isso automaticamente.", "label": "Intervalo de Amostragem" @@ -1440,7 +1470,7 @@ "label": "Janela de Suavização" }, "start_duration_threshold": { - "doc": "A energia deve permanecer acima do limite de partida por tanto tempo para confirmar uma partida real, evitando que interruptores liga/desliga em frações de segundo iniciem um ciclo.", + "doc": "A potência deve permanecer acima do Limiar de Início durante este tempo para confirmar uma partida real, evitando que comutações liga/desliga em frações de segundo iniciem um ciclo.", "label": "Duração de Início" }, "start_energy_threshold": { @@ -1448,7 +1478,7 @@ "label": "Energia de Início" }, "start_threshold_w": { - "doc": "A potência deve subir acima deste nível para confirmar que um ciclo foi iniciado. Definir um valor muito baixo causa falsas partidas na energia de espera; programas muito altos e de início lento (enchimento a frio) são perdidos. O mecanismo de sugestão define isso logo acima da potência ativa mais baixa observada pela máquina.", + "doc": "A potência deve subir acima deste nível para confirmar que um ciclo foi iniciado. Defini-lo demasiado baixo causa falsas partidas a partir da energia em espera; defini-lo demasiado alto faz perder os programas de início lento (enchimento a frio). O mecanismo de sugestão define isto logo acima da potência ativa mais baixa observada na máquina.", "label": "Limiar de Início" }, "stop_threshold_w": { @@ -1520,7 +1550,7 @@ }, "keep_min_score": { "label": "Score mín de correspondência", - "doc": "Pontuação de similaridade mínima que um candidato precisa para permanecer em jogo. A predefinição 0,1 é deliberadamente permissiva - admite mesmo candidatos fracos e depende das fases seguintes para encontrar a melhor correspondência. Aumente para eliminar perfis improváveis mais cedo; reduza para ampliar o pool inicial de candidatos." + "doc": "Pontuação de similaridade mínima que um candidato precisa para permanecer na disputa pela correspondência. A predefinição 0,1 é deliberadamente permissiva - admite mesmo candidatos fracos e depende das fases seguintes para encontrar a melhor correspondência. Aumente para eliminar perfis improváveis mais cedo; reduza para ampliar o pool inicial de candidatos." }, "dtw_blend": { "label": "Fusão Warp", @@ -1550,13 +1580,17 @@ "label": "Intervalo do Perfil de Potência", "doc": "Tamanho do intervalo para o atributo do sensor power_profile por perfil (o array plano de watts médios por slot consumido por planeadores externos como EMHASS e tibber_prices). Intervalos menores mantêm os picos de potência curtos nítidos; intervalos maiores suavizam a forma. Predefinição: 15 min. Apenas de leitura; não afeta a deteção." }, + "notify_live_silent": { + "label": "Atualizações Silenciosas em Tempo Real", + "doc": "Apenas iOS. Atualiza o progresso em tempo real de forma discreta: cada atualização chega sem som nem vibração, com uma prioridade mais baixa que poupa bateria e que o iOS pode agrupar durante alguns instantes. A primeira atualização de um ciclo (a que inicia a Atividade em Direto) e as notificações de início e de fim continuam a alertar como habitualmente. Desative esta opção para ser alertado em cada atualização." + }, "notify_live_sticky": { "label": "Manter a Notificação em Tempo Real ao Tocar", "doc": "Apenas Android. Torna a notificação de progresso em tempo real persistente (fixada) para que ao tocar nela não a dispense. Desativado mantém o comportamento padrão em que o toque a dispensa." }, "notify_live_click_action": { - "label": "Destino ao Tocar na Notificação em Tempo Real", - "doc": "Apenas Android. Para onde abre o toque na notificação de progresso em tempo real (por exemplo, /lovelace/lavandaria, ou um URL completo) em vez da página inicial da aplicação. Deixe em branco para a predefinição." + "label": "Destino ao Tocar na Notificação", + "doc": "Para onde abre um toque numa notificação deste eletrodoméstico, no Android e no iOS. Em branco abre o painel WashData neste eletrodoméstico. Introduza o caminho de um painel (por exemplo, /lovelace/laundry) ou um URL completo para abrir outro destino, ou \"none\" para não abrir nada ao tocar." }, "door_opens_at_end": { "label": "Porta Abre Automaticamente no Fim", @@ -1569,6 +1603,10 @@ "profile_evidence_sources": { "label": "Ciclos que moldam um programa", "doc": "Que ciclos são usados para construir a curva de potência de cada programa e para comparar um ciclo terminado com ela. Ao desmarcar um tipo, este deixa de moldar os seus programas sem que nada seja apagado - os ciclos permanecem na sua lista de Ciclos e podem continuar a ser rotulados ou removidos. Útil se não confiar nos dados importados. As estatísticas não são afetadas: contabilizam sempre apenas os ciclos que esta máquina realmente executou. Desmarcar tudo é ignorado, pois um programa sem ciclos por trás nunca poderia corresponder." + }, + "energy_price_dynamic": { + "label": "Custo Ponderado no Tempo", + "doc": "Cobra cada ciclo ao preço em vigor em cada momento em que decorreu, em vez do preço único do instante em que terminou. Só se aplica a uma entidade de preço (um preço estático não pode variar). Não precisa de configuração adicional: o preço é acompanhado em direto enquanto o ciclo decorre e Processar histórico, em Diagnósticos, recalcula o custo dos ciclos antigos a partir do recorder. Desativado = o comportamento clássico, um preço fixado no fim." } }, "setting_group": { @@ -1680,7 +1718,8 @@ "splitting": "A dividir…", "deleting": "A eliminar…", "imported": "Importado", - "preparing": "A preparar…" + "preparing": "A preparar…", + "unlabelling": "A remover rótulo…" }, "tab": { "advanced": "Avançado", @@ -1764,6 +1803,7 @@ "recording_stopped": "Gravação parada", "review_saved": "Revisão guardada", "settings_conflicts": "Corrija os conflitos de definições destacados antes de guardar.", + "settings_no_changes": "Nenhuma alteração para guardar", "settings_reverted": "Definições revertidas; a integração está a recarregar", "settings_saved": "Definições guardadas; a integração está a recarregar", "suggestions_applied": "Sugestões aplicadas; a integração está a recarregar", @@ -1818,7 +1858,11 @@ "export_selective_done": "Exportação transferida", "import_selective_done": "Importados {profiles} perfil(is) e {cycles} ciclo(s)", "hist_csv_required": "Carregue primeiro um ficheiro CSV ou cole o respetivo conteúdo", - "file_read_failed": "Não foi possível ler esse ficheiro" + "file_read_failed": "Não foi possível ler esse ficheiro", + "unlabel_done": "Rótulo removido de {count} ciclo(s)", + "unlabel_failed": "Falha ao remover o rótulo: {error}", + "odometer_saved": "Total de ciclos guardado", + "odometer_save_failed": "Não foi possível guardar o total: {error}" }, "suggestion": { "both_agree": "WashData recomenda", @@ -1865,10 +1909,6 @@ "higher": "Confirma apenas correspondências de alta confiança; mais ciclos ficam sem etiqueta", "lower": "Corresponde a mais ciclos; alguns podem estar ligeiramente errados" }, - "running_dead_zone": { - "higher": "Ignora breves picos de potência em repouso – menos sensível ao ruído", - "lower": "Responde a impulsos mais curtos – deteta atividade transitória rápida" - }, "start_threshold_w": { "higher": "Evita arranques falsos por breves picos de potência", "lower": "Deteta programas de baixa potência; pode ser ativado por ruído" @@ -1893,6 +1933,8 @@ "end_repeat_count": "{false}/{total} ciclos limpos ({pct}%) tiveram uma pausa interna >60s que foi retomada; exigir {val} confirmação(ões) de fim.", "learning_confidence": "Confiança p05 de {cycles} ciclos rotulados pelo utilizador ({p05}); abaixo disto, é pedida uma verificação.", "match_interval": "Com base na cadência de atualização observada (mediana={median}s) * 10.", + "match_interval_capped": "Limitado para que {persistence} correspondências consecutivas caibam em {pct}% do programa mais curto ({shortest}s); só a cadência de atualização (mediana={median}s) teria sugerido um intervalo mais longo.", + "match_interval_floored": "Só por si, o programa mais curto ({shortest}s) limitaria este valor a {cap}s, abaixo do mínimo de {minimum}s, pelo que fica nesse mínimo: {persistence} correspondências consecutivas demoram {budget}s, ou seja, mais de {pct}% desse programa. A correspondência só é executada quando chega uma leitura (mediana={median}s), pelo que o mínimo não tem custo.", "max_duration_ratio": "Com base nas durações dos ciclos rotulados (p95={p95}).", "min_active": "Com base na potência ativa mínima ({min}W) observada no último ciclo.", "min_duration_ratio": "Mantido o mais baixo possível para que um programa seja reconhecido cedo no ciclo; os limiares de confiança e ambiguidade evitam decisões prematuras.", @@ -1950,10 +1992,11 @@ "date_tip": "Data e hora de início do ciclo.", "duration_tip": "Tempo total de execução do ciclo, do início ao fim.", "energy_tip": "Energia total consumida (kWh). Calculada integrando a potência ao longo do tempo.", - "cost_tip": "Custo de energia deste ciclo, fixado na conclusão usando o preço então em vigor (energia x preço por kWh). Defina um preço nas Definições para o preencher.", + "cost_tip": "Custo de energia deste ciclo, fixado na conclusão. Com o Custo Ponderado no Tempo ativado, a curva de potência é cobrada ao preço em vigor em cada momento; caso contrário, energia x o preço único em vigor no fim. Defina um preço nas Definições para o preencher.", "confidence_tip": "Confiança de correspondência do perfil (0-100%). Quão próxima a curva de potência do ciclo correspondeu ao programa identificado.", "health_tip": "Saúde do ciclo por ML (mais alto = melhor). Clique num ciclo para o inspecionar e rever.", - "flags_tip": "Indicadores de revisão, anomalia e origem do ciclo. Passe o cursor sobre um ícone para ver o detalhe." + "flags_tip": "Indicadores de revisão, anomalia e origem do ciclo. Passe o cursor sobre um ícone para ver o detalhe.", + "cost_dynamic_tip": "Ponderado no tempo: cobrado ao preço em vigor durante o ciclo (preço efetivo {price} {cur}/kWh)." }, "stat": { "avg": "méd.", @@ -1980,9 +2023,9 @@ "finished": "O ciclo atingiu um estado final e terminou." }, "store": { + "search_ph2": "Pesquisar por marca ou modelo…", "your_model_tip": "Este é o electrodoméstico que declarou nas Definições", "your_model": "O seu", - "search_brand_ph": "Pesquisar por marca…", "programs_count": "Programas: {n}", "browse": "Explorar", "device": "Dispositivo", @@ -2027,7 +2070,8 @@ "golden": "Reprocessamento: marcação de ciclos de referência", "suggestions": "Reprocessamento: sugestões", "ml_training": "Reprocessamento: treino de ML", - "health": "Reprocessamento: saúde dos ciclos" + "health": "Reprocessamento: saúde dos ciclos", + "costs": "Reprocessamento: custos de energia" }, "pg_sweep": { "optimize": "Otimizar: {param}" diff --git a/custom_components/ha_washdata/translations/panel/ro.json b/custom_components/ha_washdata/translations/panel/ro.json index b060a516..8e66770f 100644 --- a/custom_components/ha_washdata/translations/panel/ro.json +++ b/custom_components/ha_washdata/translations/panel/ro.json @@ -1,5 +1,7 @@ { "badge": { + "quiet_tail_tip": "Spre final, acest program rămâne în liniște aproximativ {mins} min, apoi consumă aproximativ {watts} W timp de {secs} s înainte de a se încheia. Observat în {seen} din {measured} cicluri măsurate: aparatul nu face asta la fiecare utilizare.", + "quiet_tail": "liniște finală ~{mins} min", "artifact_tip": "{n} anomalii detectate (de exemplu, ușa deschisă la jumătatea ciclului) – deschideți pentru a le vedea pe grafic", "auto": "(detectat automat)", "built_in_tag": "integrat", @@ -29,11 +31,14 @@ "approved": "Aprobat", "awaiting": "În așteptarea aprobării", "imported_tip": "Importat din magazinul comunității. Folosit doar pentru potrivire, necontabilizat în statistici.", + "unmatchable": "potrivire imposibilă", "not_importable": "n/a aici", "exists": "există deja", - "backfilled_tip": "Detectat într-un istoric de putere importat. Influențează doar potrivirea programelor, nu este contabilizat în statistici." + "backfilled_tip": "Detectat într-un istoric de putere importat. Influențează doar potrivirea programelor, nu este contabilizat în statistici.", + "armed": "(se aplică ciclului următor)" }, "btn": { + "reset_zoom": "Resetați zoomul", "set_brand_model": "Stabiliți marca și modelul", "refresh_catalog": "Actualizați catalogul", "add_device": "+ Adăugați dispozitiv", @@ -167,6 +172,8 @@ "trim": "Tăiați", "trim_split": "Tăiați / Divizați", "undo": "Anulați", + "unlabel_selected": "Eliminați eticheta selecției ({n})", + "unlabel_selected_tip": "Elimină eticheta de profil de la ciclurile selectate. Rămân în istoricul dvs. și pot fi etichetate din nou mai târziu; doar nu mai influențează acest profil.", "use": "Utilizați", "wipe_all": "Ștergeți toate datele", "delete_selected": "Ștergeți selecția ({n})", @@ -174,7 +181,6 @@ "pg_apply_best": "Aplică cel mai bun", "pg_apply_to_settings": "Salvează în setări", "pg_apply_to_settings_tip": "Copiază valorile pe care le-ai modificat aici în setările active ale acestui dispozitiv", - "search": "Căutați", "import": "Importați", "disconnect": "Deconectați", "connect_github": "Conectați GitHub", @@ -201,7 +207,8 @@ "hist_read_recorder": "Citiți din Home Assistant", "hist_scan": "Căutați cicluri", "hist_import_n": "Importați {n} cicluri", - "hist_goto_cycles": "Vedeți ciclurile" + "hist_goto_cycles": "Vedeți ciclurile", + "save_total_cycles": "Salvați totalul" }, "conflict": { "anti_wrinkle_exit": { @@ -214,6 +221,12 @@ }, "attn_sub": "Rezolvați conflictele înainte de salvare", "attn_title": "Conflicte de setări: {n}", + "price_entity": { + "power_sensor": "Acesta este Senzorul de Putere al dispozitivului, nu un preț. O entitate de preț are prioritate față de Prețul Static Energie de mai jos, așa că ciclurile ar fi taxate la wați pe kWh - goliți acest câmp.", + "energy_sensor": "Aceasta este Entitatea contor de energie a dispozitivului, nu un preț. O entitate de preț are prioritate față de Prețul Static Energie de mai jos, așa că ciclurile ar fi taxate la valoarea contorului pe kWh - goliți acest câmp.", + "device_class": "Acest senzor măsoară {dc}, nu un preț pe kWh. O entitate de preț are prioritate față de Prețul Static Energie de mai jos - goliți acest câmp sau indicați un senzor de tarif.", + "unit": "Acest senzor afișează valori în {unit}, ceea ce nu este un preț pe kWh. O entitate de preț are prioritate față de Prețul Static Energie de mai jos - goliți acest câmp sau indicați un senzor de tarif." + }, "settings_banner": "Conflicte de setări: {n}. Verificați secțiunile evidențiate și remediați-le înainte de salvare.", "settings_banner_btn": "Salt la primul", "confidence": { @@ -301,7 +314,7 @@ "pg_settings_source": "Sursa setărilor", "phase_catalog": "Catalog de faze", "playground": "Zonă de test", - "process_history": "Istoricul procesării", + "process_history": "Procesare istoric", "settings_history": "Istoric setări", "status": "Stare", "status_graph": "Grafic de stare", @@ -312,12 +325,12 @@ "wipe_history": "Ștergeți istoricul", "recommendations": "Recomandări ({n})", "pg_detection_params": "Setări de detectare", - "pg_timeline": "Cronologia evenimentelor", "pg_outcome": "Rezultatul simulării", "pg_across_cycles": "În toate ciclurile tale", "community_store": "Magazinul comunității", "online_account": "Magazinul comunității și funcții online", - "import_power_history": "Importați istoricul de putere" + "import_power_history": "Importați istoricul de putere", + "service_status": "Starea întreținerii" }, "health": { "fair": "Calitate acceptabilă a profilului", @@ -325,6 +338,10 @@ "poor": "⚠ Calitate slabă a profilului" }, "lbl": { + "envelope_position_tip": "Poziția pe curba înregistrată a programului potrivit, {pct}%. Este măsurată prin alinierea acestui ciclu la profil în loc de numărarea timpului, așa că rămâne corectă atunci când un ciclu durează mai mult sau mai puțin decât de obicei. Se actualizează cât timp aparatul este în liniște, deci poate rămâne în urmă în timpul unei faze active.", + "envelope_position": "curbă {pct}%", + "aria_playground_chart3": "Grafic interactiv al puterii ciclului: treci cu cursorul pentru a citi timpul/puterea, derulează sau folosește două degete pentru a mări, trage pentru a te deplasa", + "device_unresolved": "Dispozitiv indisponibil ({id})", "drag_to_resize": "Trageți pentru redimensionare", "actions": "Acțiuni", "activity": "Activitate", @@ -460,7 +477,7 @@ "profile_index": "index profil →", "profile_name": "Numele profilului", "program": "Program", - "program_tip": "Înlocuiți profilul potrivit ciclului curent. Detectarea automată permite integrării să aleagă automat cea mai bună potrivire. Fixați un program specific pentru a forța potrivirea când detectarea automată este incorectă sau știți ce rulează.", + "program_tip": "Înlocuiți profilul potrivit ciclului curent. Detectarea automată permite integrării să aleagă automat cea mai bună potrivire. Fixați un program specific pentru a forța potrivirea când detectarea automată este incorectă sau știți ce rulează. Alegeți unul înainte de a porni aparatul și va fi aplicat imediat ce începe ciclul următor.", "progress": "Progres", "raw_socket": "Socket brut", "read_only": "Doar citire", @@ -495,6 +512,7 @@ "stage4": "Etapa 4 – concordanță", "status": "Stare", "tail_trim": "Tăiere finală (s)", + "tap_to_dismiss": "atingeți graficul pentru a închide", "timer_auto_pause": "Pauză automată", "timer_min": "min", "timer_msg_placeholder": "Mesaj (opțional, {device}/{program}/{minutes})", @@ -531,9 +549,7 @@ "pg_time_left_model": "Timp rămas (model)", "pg_time_left_model_tip": "Timp rămas estimat de model (estimator de fază + combinație ML), nu o numărătoare inversă fixă", "phase": "Fază", - "pg_mode_simulate": "Simulează", "pg_mode_history": "Testează pe istoric", - "pg_mode_sweep": "Scanare", "pg_ev_detected": "Detectat", "pg_ev_match": "Potrivire confirmată", "pg_ev_match_changed": "Potrivire schimbată", @@ -572,14 +588,11 @@ "pg_stress_idle_w_desc": "Înlocuiește nivelul de repaus detectat automat (lăsați gol pentru automat).", "pg_stress_toggle": "Testați terminarea în repaus", "pg_stress_toggle_desc": "Simulează aparatul menținut la consumul său în standby după terminarea înregistrării – arată dacă și când WashData oprește ciclul.", - "pg_sweep_2d": "Hartă termică 2D", - "pg_sweep_param_y": "Al 2-lea parametru", "pg_obj_match": "Acuratețea potrivirii", "pg_obj_endtiming": "Acuratețea momentului de încheiere", "pg_obj_falseend": "Rata de sfârșituri false", "pg_obj_overrun": "Abatere de durată", "pg_obj_ambiguity": "Rata de ambiguitate", - "pg_best_combo": "cea mai bună combinație", "pg_mode_optimize": "Optimizează", "pg_group_matching": "Potrivirea programului", "pg_zoom_hint": "dublu clic pentru a reseta zoomul", @@ -658,7 +671,12 @@ "task_history_import_apply": "Se importă ciclurile", "evidence_real_cycles": "Cicluri rulate de această mașină", "evidence_reference_cycles": "Descărcate din magazinul comunității", - "evidence_backfill_cycles": "Găsite într-un istoric de putere importat" + "evidence_backfill_cycles": "Găsite într-un istoric de putere importat", + "zoom_hint_touch": "folosiți două degete pentru zoom", + "zoom_hint_touch_zoomed": "folosiți două degete pentru zoom · dublă atingere pentru resetare", + "cycles_since_service": "{since} / {total} cicluri", + "total_cycles_run": "cicluri efectuate în total", + "correct_total_cycles": "Corectați totalul" }, "log": { "all_levels": "Toate nivelurile", @@ -708,7 +726,7 @@ "merge_cycles": "Îmbinați {n} cicluri", "new_group": "Grup de profiluri nou", "new_phase": "Fază nouă", - "process_history_msg": "Reexecutați potrivirea, actualizați sugestiile, reantrenați ML (dacă este activat) și recalculați starea ciclurilor pentru toate ciclurile stocate. Acest lucru poate dura ceva timp.", + "process_history_msg": "Reexecutați potrivirea, actualizați sugestiile, reantrenați ML (dacă este activat), recalculați costul ciclurilor pe baza prețurilor energiei înregistrate și recalculați starea ciclurilor pentru toate ciclurile stocate. Acest lucru poate dura ceva timp.", "process_history_ok": "Procesare", "process_history_title": "Procesare Istoric", "process_recording": "Procesați înregistrarea", @@ -741,6 +759,7 @@ "history_import": "Importați istoricul de putere" }, "msg": { + "pg_canvas_empty3": "Alege un ciclu de mai sus și apasă Rulează pentru a-l simula. Apoi treci cu cursorul pentru a citi valorile, derulează sau folosește două degete pentru a mări și trage pentru a te deplasa.", "tail_trim_hint": "Numărul de secunde de eliminat de la sfârșit", "store_sibling_hint": "Nu există nimic partajat pentru modelul dvs. exact? Un model foarte apropiat de la aceeași marcă este de obicei un bun punct de plecare.", "store_declare_appliance": "Indicați în WashData ce aparat dețineți, iar această filă va afișa configurațiile pe care alte persoane le-au partajat pentru el. Puteți și să scrieți o marcă mai sus pentru a explora.", @@ -754,7 +773,7 @@ "artifact_spike_detail": "A depășit banda de putere obișnuită timp de ~{n}s.", "auto_label_intro": "Atribuiți profiluri ciclurilor neetichetate a căror încredere de potrivire depășește pragul.", "automations_intro": "WashData declanșează evenimente {start} / {end} și expune entități, astfel că notificările și acțiunile sunt cel mai bine construite ca automatizări normale Home Assistant. Automatizările care utilizează acest dispozitiv apar mai jos.", - "cleanup_intro": "Toate ciclurile etichetate suprapuse. Bifați valorile aberante și ștergeți pentru a curăța profilul.", + "cleanup_intro": "Toate ciclurile etichetate suprapuse. Bifați valorile aberante, apoi eliminați-le eticheta (rămân în istoric, dar nu mai influențează acest profil) sau ștergeți-le definitiv.", "clear_debug_hint": "Eliminați datele de depanare stocate pentru a elibera spațiu.", "collecting_data": "Se colectează date. Cicluri încă necesare înainte ca reglajul fin să poată începe: {need} ({current}/{min}).", "compare_overlay_profiles": "Suprapuneți profiluri (estompat)", @@ -853,7 +872,7 @@ "preferences_admin": "Preferințe, panou și control acces", "preferences_adv": "Preferințe", "prefs_personal": "Se aplică numai contului dvs. Home Assistant.", - "process_history_hint": "Reexecutați potrivirea pe toate ciclurile stocate, actualizați sugestiile de reglaj, reantrenați modelele ML (dacă sunt activate) și recalculați starea ciclurilor. Rulați după un lot de recenzii.", + "process_history_hint": "Reexecutați potrivirea pe toate ciclurile stocate, actualizați sugestiile de reglaj, reantrenați modelele ML (dacă sunt activate), recalculați costul ciclurilor pe baza prețurilor energiei înregistrate și recalculați starea ciclurilor. Rulați după un lot de revizuiri.", "profile_deleted": "Profil șters", "profile_poor_health_detail": "Ciclurile atribuite acestui profil au forme inconsistente sau încredere scăzută. Luați în considerare reconstruirea plicului sau revizuirea ciclurilor etichetate.", "profiles_intro": "Faceți clic pe un profil pentru statistici, faze și curățare. Grupați programe aproape identice (aceeași formă/durată, temperatură sau centrifugare diferite) pentru ca potrivirea să aleagă în mod fiabil între ele.", @@ -936,9 +955,10 @@ "advisory_poor_health": "'{name}' are un scor de potrivire scăzut - ciclurile sale recente variază mult sau se potrivesc slab. Examinați ciclurile sau reînregistrați profilul pentru ca recunoașterea și estimările de timp să rămână exacte.", "advisory_shape_drift": "'{name}' a deviat semnificativ față de forma inițială a puterii. Comportamentul aparatului s-ar putea să se fi schimbat în timp (de ex. calcar, uzură). Luați în considerare reînregistrarea acestui profil cu cicluri recente.", "advisory_shape_drift_corr": "'{name}' a deviat semnificativ față de forma inițială a puterii (corelație {corr}). Comportamentul aparatului s-ar putea să se fi schimbat în timp (de ex. calcar, uzură). Luați în considerare reînregistrarea acestui profil cu cicluri recente.", + "advisory_unmatchable": "'{name}' nu va putea fi niciodată potrivit: nu are în spate niciun ciclu cu date de putere, așa că WashData nu are cu ce să compare un ciclu în desfășurare. Etichetați un ciclu al acestui program (sau înregistrați unul) pentru a-l readuce în procesul de potrivire.", + "advisory_unmatchable_no_duration": "'{name}' nu va putea fi niciodată potrivit: are un ciclu cu date de putere, dar fără o durată utilizabilă, așa că WashData nu are nicio durată după care să dimensioneze o potrivire. Înregistrați din nou acest program sau setați-i durata estimată, pentru a-l readuce în procesul de potrivire.", "pg_no_alerts": "Nicio problemă detectată în această rulare.", "pg_override_active": "Se folosesc setările editate față de cele actuale", - "pg_history_intro": "Reluați ciclurile recente prin detectorul și sistemul de potrivire reale. Modificați mai întâi o setare de detectare în Simulează pentru a vedea aici o comparație înainte/după.", "pg_history_empty": "Apăsați Rulați pentru a relua ciclurile.", "pg_history_summary": "{detected}/{total} detectate · {correct} potrivite corect", "pg_sweep_no_metric": "Date insuficiente pentru a evalua acest obiectiv.", @@ -1015,7 +1035,11 @@ "hist_scan_failed": "Analiza a eșuat.", "hist_scan_expired": "Acea analiză nu mai este disponibilă. Reluați analiza.", "hist_import_failed": "Importul a eșuat.", - "imported_history_readonly": "Detectat într-un istoric de putere importat. Influențează potrivirea programelor, dar nu este contabilizat în statisticile dvs. și nu poate fi tăiat sau divizat. Etichetați-l pentru a indica programul." + "imported_history_readonly": "Detectat într-un istoric de putere importat. Influențează potrivirea programelor, dar nu este contabilizat în statisticile dvs. și nu poate fi tăiat sau divizat. Etichetați-l pentru a indica programul.", + "odometer_intro": "Acest total doar crește. Ștergerea înregistrării unui ciclu nu îl modifică și continuă să numere dincolo de limita istoricului păstrat, astfel încât mementourile de service rămân corecte.", + "odometer_correct_hint": "Setați această valoare dacă WashData a înregistrat un ciclu care nu a avut loc niciodată sau dacă aparatul efectuase deja cicluri înainte de instalarea WashData.", + "no_reminders_set": "Încă nu este setat niciun memento de service.", + "toast_program_armed": "Program pregătit pentru ciclul următor: {program}" }, "phase_desc": { "anti_crease": "Tumblări scurte ocazionale după finalizare pentru a reduce șifonarea.", @@ -1113,6 +1137,10 @@ "label": "Eliberare la liniște în uscarea pasivă", "doc": "După ce ciclul depășește durata așteptată, cât timp trebuie să rămână silențioasă mașina de spălat vase (sub Prag de Oprire) înainte ca WashData să nu mai aștepte o evacuare finală și să încheie ciclul. Măriți-l dacă mașina are o fază lungă de uscare silențioasă înainte de o evacuare finală întârziată care nu este detectată - o fereastră mai largă permite duratei învățate să urmeze variația sezonieră (apă de alimentare mai rece = cicluri mai lungi) în loc să se fixeze pe media veche. Doar scurtează așteptarea față de plafonul intern de 30 de minute pentru vârful de final, fără a o prelungi vreodată." }, + "anti_crease_finalize_ratio": { + "label": "Raport de finalizare anti-șifonare", + "doc": "Cât de departe în durata așteptată a programului corespunzător trebuie să ajungă un ciclu înainte ca WashData să poată stabili că rotirea lentă de putere redusă pe care o observă este coada anti-șifonare și să încheie ciclul acolo. Durata așteptată este media programului, așa că la un uscător al cărui timp de uscare cu senzor urmează încărcătura majoritatea ciclurilor nu ating niciodată 98% din acea medie, iar coada rămâne în așteptare până la timpul de expirare de rezervă, deseori cu 15 până la 40 de minute după ce mașina s-a oprit efectiv. Reduceți această valoare (de exemplu, 0,75) la un astfel de uscător. Lăsați mașinile de spălat rufe la valoarea implicită: o mașină de spălat petrece cea mai mare parte a ciclului sub Puterea Maximă Anti-Șifonare, deci această fracțiune este singurul lucru care separă o fază liniștită de la mijlocul spălării de coadă, iar reducerea ei poate înjumătăți o spălare. Lăsați gol pentru valoarea implicită (0,98). Această setare este diferită de Raportul de terminare inteligentă, care condiționează o verificare separată de finalizare timpurie." + }, "anti_wrinkle_enabled": { "doc": "Recunoașteți impulsurile scurte de rufe de putere redusă pe care le emite un uscător după faza principală de căldură și mențineți-le atașate la ciclul terminat în loc să le citiți ca cicluri noi.", "label": "Activați Detectarea Anti-Șifonare" @@ -1134,7 +1162,7 @@ "label": "Putere Maximă Anti-Șifonare" }, "auto_label_confidence": { - "doc": "Dacă scorul meciului la sfârșitul ciclului este egal sau peste acesta, programul este etichetat automat fără nicio solicitare de confirmare. Ridicați-l pentru a necesita o mai mare certitudine înainte de auto-etichetare; coborâți-l pentru a automatiza mai mult. Funcționează împreună cu Learning Confidence de mai jos.", + "doc": "Dacă scorul de potrivire la sfârșitul ciclului este egal sau peste acesta, programul este etichetat automat fără nicio solicitare de confirmare. Ridicați-l pentru a necesita o mai mare certitudine înainte de auto-etichetare; coborâți-l pentru a automatiza mai mult. Funcționează împreună cu Încredere Învățare de mai jos.", "label": "Încredere Etichetare Automată" }, "auto_maintenance": { @@ -1145,6 +1173,10 @@ "doc": "Ciclurile mai scurte decât aceasta sunt eliminate ca cicluri fantomă (testare, deschiderea ușii pentru a adăuga un șosetă).", "label": "Durata Minimă a Ciclului" }, + "curve_preroll_seconds": { + "label": "Preambulul curbei", + "doc": "Câte secunde de citiri provenite din încercări de pornire întrerupte poate include WashData la începutul curbei unui ciclu. Curba unui ciclu începe în mod normal la încercarea de pornire care reușește în cele din urmă, așa că la o mașină care încearcă de câteva ori înainte de a porni efectiv (selectarea programului, blocarea ușii, prima umplere) primele minute de activitate reală lipsesc din fiecare curbă. Setați această valoare puțin peste cea mai lungă astfel de pornire pregătitoare a mașinii dumneavoastră (300 s sunt mai mult decât suficiente în majoritatea cazurilor). 0 dezactivează funcția. Rețineți că activarea ei mută mai devreme startul înregistrat, astfel încât ciclurile înregistrate înainte și după modificare vor avea durate diferite pentru același program; mediile învățate vor devia până când ciclurile mai vechi ies din istoric." + }, "delay_confirm_seconds": { "doc": "Puterea trebuie să rămână în banda de așteptare mult timp înainte ca aparatul să fie tratat ca așteptând să pornească și nu ca fiind în funcțiune.", "label": "Secunde de Confirmare" @@ -1182,7 +1214,7 @@ "label": "Număr de Repetări la Sfârșit" }, "energy_price_entity": { - "doc": "Senzor cu prețul curent al energiei electrice pe kWh (de exemplu, un tarif dinamic). Are prioritate față de prețul static de mai jos. Fiecare ciclu îngheață prețul în vigoare când s-a terminat.", + "doc": "Senzor cu prețul curent al energiei electrice pe kWh (de exemplu, un tarif dinamic). Trebuie să fie un preț, nu entitatea de putere sau de energie a prizei: are prioritate față de prețul static de mai jos, așa că un contor de kWh pus aici taxează fiecare ciclu la valoarea contorului în loc de tariful dumneavoastră. Cu Cost Ponderat în Timp activat, fiecare ciclu este taxat la prețul în vigoare în fiecare moment în care a rulat; altfel, i se fixează prețul în vigoare când s-a terminat.", "label": "Entitate Preț Energie" }, "energy_price_static": { @@ -1210,7 +1242,7 @@ "label": "Inversați Declanșatorul Extern (declanșare la OPRIT)" }, "learning_confidence": { - "doc": "Dacă scorul meciului se situează între acesta și Auto-Label Confidence, o notificare de feedback vă solicită să verificați programul identificat. Sub acest scor meciul este prea incert pentru a ieși la suprafață. Trebuie menținut sub nivelul de încredere pentru etichete automate.", + "doc": "Dacă scorul de potrivire se situează între acesta și Încredere Etichetare Automată, o notificare de feedback vă solicită să verificați programul identificat. Sub acest scor, potrivirea este prea incertă pentru a fi afișată. Trebuie menținut sub Încredere Etichetare Automată.", "label": "Încredere Învățare" }, "linked_device": { @@ -1278,7 +1310,7 @@ "label": "Declanșați Evenimente HA pentru Notificări" }, "notify_icon": { - "doc": "Pictogramă mdi opțională pentru notificare (de exemplu, mdi:washing-machine). Lăsați necompletat pentru valoarea prestabilită a platformei.", + "doc": "Pictograma mică afișată pe notificare (de exemplu, mdi:washing-machine). Android o desenează în bara de stare; iOS o afișează în locul pictogramei aplicației și necesită aplicația însoțitoare 2026.8 sau mai nouă. Lăsați necompletat pentru valoarea prestabilită a platformei.", "label": "Pictogramă Notificare" }, "notify_live_chronometer": { @@ -1378,7 +1410,7 @@ "label": "Prag de Potrivire" }, "profile_unmatch_threshold": { - "doc": "Dacă un meci în direct la mijlocul ciclului scade sub acest scor, identificarea provizorie este șters. Păstrați-l puțin sub Pragul de potrivire, astfel încât o scurtă scădere în similitudine să nu întoarcă afișajul la neegalat.", + "doc": "Dacă o potrivire live de la mijlocul ciclului scade sub acest scor, identificarea provizorie este ștearsă. Păstrați-l puțin sub Pragul de potrivire, astfel încât o scădere scurtă a similitudinii să nu readucă afișajul la starea fără potrivire.", "label": "Prag de Nepotrivire" }, "progress_reset_delay": { @@ -1389,10 +1421,6 @@ "doc": "În câteva secunde, o pompă poate funcționa continuu înainte de a fi semnalată ca posibil blocată (declanșează evenimentul de pompă blocată).", "label": "Durata Pompei Blocate" }, - "running_dead_zone": { - "doc": "După începerea unui ciclu, scăderile de putere din această fereastră sunt ignorate. Mașinile de spălat se umplu cu apă rece (scăzând aproape de 0 W înainte de încălzire) - fără această protecție, faza de umplere arată ca un sfârșit de ciclu. Acest lucru NU omite datele: traseul complet de putere este înregistrat de la T=0. Motorul de sugestii măsoară modelul real de pornire al mașinii și îl dimensionează automat.", - "label": "Zonă Moartă în Funcționare" - }, "sampling_interval": { "doc": "Timp așteptat între citirile senzorului - utilizat pentru a dimensiona fereastra de netezire și pentru a începe corect să renunțe. Fiecare actualizare a senzorului este capturată indiferent de această valoare; calibrează doar calculele din aval. Motorul de sugestii măsoară cadența reală a senzorului dvs. din ciclurile trecute și o setează automat.", "label": "Interval de Eșantionare" @@ -1418,7 +1446,7 @@ "label": "Energia de Pornire" }, "start_threshold_w": { - "doc": "Puterea trebuie să crească peste acest nivel pentru a confirma că a început un ciclu. Setarea prea scăzută cauzează porniri false de la puterea de așteptare; Programele prea mari și cu pornire lentă (umplere la rece) sunt ratate. Motorul de sugestie setează acest lucru chiar peste cea mai mică putere activă observată a mașinii.", + "doc": "Puterea trebuie să crească peste acest nivel pentru a confirma că a început un ciclu. O valoare prea mică provoacă porniri false din cauza puterii în așteptare; o valoare prea mare face ca programele cu pornire lentă (umplere la rece) să fie ratate. Motorul de sugestii setează această valoare chiar peste cea mai mică putere activă observată a mașinii.", "label": "Prag de Pornire" }, "stop_threshold_w": { @@ -1520,13 +1548,17 @@ "label": "Interval Profil Putere", "doc": "Dimensiunea intervalului pentru atributul senzorului power_profile per profil (tabloul plat de wați medii per slot consumat de planificatoare externe precum EMHASS și tibber_prices). Intervalele mai mici păstrează vârfurile scurte de putere clare; intervalele mai mari netezesc forma. Implicit 15 min. Doar la citire; nu afectează detectarea." }, + "notify_live_silent": { + "label": "Actualizări silențioase în timp real", + "doc": "Numai iOS. Reîmprospătează discret progresul în timp real: fiecare actualizare sosește fără sunet sau vibrații, cu o prioritate mai redusă, care economisește bateria și pe care iOS o poate grupa pentru scurt timp. Prima actualizare dintr-un ciclu (cea care pornește Activitatea live) și notificările de început și de final alertează în continuare ca de obicei. Dezactivați această opțiune pentru a fi alertat la fiecare actualizare." + }, "notify_live_sticky": { "label": "Păstrați notificarea în timp real la atingere", "doc": "Numai Android. Faceți notificarea de progres în timp real persistentă (fixată) astfel încât atingerea ei să nu o respingă. Dezactivat păstrează comportamentul implicit în care o atingere o respinge." }, "notify_live_click_action": { - "label": "Destinație la atingerea notificării în timp real", - "doc": "Numai Android. Unde deschide o atingere pe notificarea de progres în timp real (de ex. /lovelace/laundry, sau un URL complet) în loc de pagina principală a aplicației. Lăsați necompletat pentru valoarea implicită." + "label": "Destinație la atingerea notificării", + "doc": "Unde se deschide o atingere pe o notificare de la acest aparat, pe Android și pe iOS. Necompletat deschide panoul WashData pentru acest aparat. Introduceți calea unui tablou de bord (de ex. /lovelace/laundry) sau un URL complet pentru a deschide altceva, sau „none” pentru nicio destinație la atingere." }, "door_opens_at_end": { "label": "Ușa se deschide automat la final", @@ -1539,6 +1571,10 @@ "profile_evidence_sources": { "label": "Ciclurile care formează un program", "doc": "Ce cicluri sunt folosite pentru a construi curba de putere a fiecărui program și pentru a compara un ciclu încheiat cu ea. Debifarea unui tip face ca acesta să nu mai modeleze programele dvs., fără a șterge nimic - ciclurile rămân în lista dvs. Cicluri și pot fi în continuare etichetate sau eliminate. Util dacă nu aveți încredere în datele importate. Statisticile nu sunt afectate: ele numără întotdeauna doar ciclurile pe care această mașină le-a rulat efectiv. Debifarea tuturor este ignorată, deoarece un program fără cicluri în spate nu s-ar putea potrivi niciodată." + }, + "energy_price_dynamic": { + "label": "Cost Ponderat în Timp", + "doc": "Taxează fiecare ciclu la prețul în vigoare în fiecare moment în care a rulat, în loc de prețul unic din momentul în care s-a terminat. Se aplică doar unei entități de preț (un preț static nu poate varia). Nu necesită configurare suplimentară: prețul este urmărit în timp real cât timp ciclul rulează, iar Procesare istoric, din Diagnostice, recalculează costul ciclurilor mai vechi pornind de la recorder. Dezactivat = comportamentul clasic, un singur preț fixat la final." } }, "setting_group": { @@ -1650,7 +1686,8 @@ "splitting": "Se divide…", "deleting": "Se șterge…", "imported": "Importat", - "preparing": "Se pregătește…" + "preparing": "Se pregătește…", + "unlabelling": "Se elimină eticheta…" }, "tab": { "advanced": "Avansat", @@ -1734,6 +1771,7 @@ "recording_stopped": "Înregistrare oprită", "review_saved": "Revizuire salvată", "settings_conflicts": "Corectați conflictele de setări evidențiate înainte de salvare.", + "settings_no_changes": "Nicio modificare de salvat", "settings_reverted": "Setări restaurate; integrarea se reîncarcă", "settings_saved": "Setări salvate; integrarea se reîncarcă", "suggestions_applied": "Sugestii aplicate; integrarea se reîncarcă", @@ -1788,7 +1826,11 @@ "export_selective_done": "Export descărcat", "import_selective_done": "Importate {profiles} profil(uri) și {cycles} ciclu(ri)", "hist_csv_required": "Încărcați mai întâi un fișier CSV sau lipiți conținutul acestuia", - "file_read_failed": "Fișierul nu a putut fi citit" + "file_read_failed": "Fișierul nu a putut fi citit", + "unlabel_done": "Etichetă eliminată de la {count} ciclu(ri)", + "unlabel_failed": "Eliminarea etichetei a eșuat: {error}", + "odometer_saved": "Totalul ciclurilor a fost salvat", + "odometer_save_failed": "Nu s-a putut salva totalul: {error}" }, "suggestion": { "both_agree": "WashData recomandă", @@ -1835,10 +1877,6 @@ "higher": "Confirmă doar potriviri de înaltă încredere; mai multe cicluri rămân neetichetate", "lower": "Potrivește mai multe cicluri; unele pot fi ușor greșite" }, - "running_dead_zone": { - "higher": "Ignoră scurte vârfuri de putere în repaus – mai puțin sensibil la zgomot", - "lower": "Răspunde la impulsuri mai scurte – detectează activitate tranzitorie rapidă" - }, "start_threshold_w": { "higher": "Evită pornirile false cauzate de scurte vârfuri de putere", "lower": "Detectează programe de putere redusă; poate fi declanșat de zgomot" @@ -1863,6 +1901,8 @@ "end_repeat_count": "{false}/{total} cicluri curate ({pct}%) au avut o pauză internă de >60s care a fost reluată; necesită {val} confirmare(i) de sfârșit.", "learning_confidence": "Încrederea p05 a {cycles} cicluri etichetate de utilizator ({p05}); sub această valoare, se solicită verificare.", "match_interval": "Pe baza cadenței de actualizare observate (mediană={median}s) * 10.", + "match_interval_capped": "Limitat astfel încât {persistence} potriviri consecutive să încapă în {pct}% din cel mai scurt program ({shortest}s); singură, cadența de actualizare (mediană={median}s) ar fi sugerat un interval mai lung.", + "match_interval_floored": "Singur, cel mai scurt program ({shortest}s) ar limita această valoare la {cap}s, sub minimul de {minimum}s, așa că este menținută la acest minim: {persistence} potriviri consecutive durează {budget}s, adică mai mult de {pct}% din acel program. Potrivirea rulează doar când sosește o citire (mediană={median}s), deci minimul nu costă nimic.", "max_duration_ratio": "Pe baza duratelor ciclurilor etichetate (p95={p95}).", "min_active": "Pe baza puterii active minime ({min}W) observate în ultimul ciclu.", "min_duration_ratio": "Păstrat cât mai mic posibil, astfel încât un program să fie recunoscut devreme în ciclu; pragurile de încredere și de ambiguitate previn confirmările premature.", @@ -1917,6 +1957,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Mașină de spălat vase: secunde de liniște după durata așteptată înainte de a elibera așteptarea evacuării de la sfârșitul ciclului", "smart_termination_duration_ratio": "Fracțiunea din durata așteptată a programului corespunzător pe care un ciclu trebuie să o atingă înainte ca Terminarea inteligentă să îl poată încheia mai devreme; reduceți-o pentru mașinile care depind de încărcătură sau de temperatură", + "anti_crease_finalize_ratio": "Fracțiunea din durata așteptată a programului corespunzător pe care un ciclu trebuie să o atingă înainte ca coada de rotire anti-șifonare să poată fi finalizată; reduceți-o la uscătoarele cu o coadă de rotire lungă, lăsați mașinile de spălat la valoarea implicită", "anti_wrinkle_enabled": "Absoarbe impulsurile de rotire de după faza principală în loc să le trateze drept cicluri noi", "anti_wrinkle_exit_power": "Puterea trebuie să scadă sub această valoare între impulsuri pentru ca modul anti-șifonare să rămână activ", "anti_wrinkle_idle_timeout": "Timp de liniște permis între două impulsuri de rotire înainte ca modul anti-șifonare să se încheie", @@ -1942,7 +1983,8 @@ "dtw_ddtw_scale": "Etapa 3: distanța de semi-saturație DDTW; mai mic = mai sensibil la formă (implicit 30)", "dtw_refine_top_n": "Etapa 3: candidați rescorați de DTW; măriți la 7-9 dacă profilul corect se clasează pe locul 4-5 (implicit 5)", "duration_scale": "Etapa 4: raportul logaritmic la care acordul de durată se înjumătățește; mai mic = penalizare mai strictă (implicit 0.175)", - "energy_scale": "Etapa 4: raportul logaritmic la care acordul de energie se înjumătățește; mai mic = penalizare mai strictă (implicit 0.25)" + "energy_scale": "Etapa 4: raportul logaritmic la care acordul de energie se înjumătățește; mai mic = penalizare mai strictă (implicit 0.25)", + "curve_preroll_seconds": "Secunde de citiri din încercări de pornire întrerupte incluse la începutul curbei unui ciclu; 0 = dezactivat" }, "col": { "profile_tip": "Numele programului corespunzător. Neetichetat înseamnă că niciun profil nu a corespuns la sfârșitul ciclului.", @@ -1950,10 +1992,11 @@ "date_tip": "Data și ora la care a început ciclul.", "duration_tip": "Durata totală de rulare a ciclului, de la început până la sfârșit.", "energy_tip": "Energia totală consumată (kWh). Calculată prin integrarea puterii în timp.", - "cost_tip": "Costul energetic al acestui ciclu, fixat la finalizare folosind prețul în vigoare atunci (energie x preț pe kWh). Setați un preț în Setări pentru a-l completa.", + "cost_tip": "Costul energetic al acestui ciclu, fixat la finalizare. Cu Cost Ponderat în Timp activat, curba de putere este taxată la prețul în vigoare în fiecare moment; altfel, energie x prețul unic în vigoare la final. Setați un preț în Setări pentru a-l completa.", "confidence_tip": "Încrederea de potrivire a profilului (0-100%). Cât de bine s-a potrivit curba de putere a ciclului cu programul identificat.", "health_tip": "Starea ciclului prin ML (mai mare = mai bine). Faceți clic pe un ciclu pentru a-l inspecta și revizui.", - "flags_tip": "Semnale de revizuire, anomalie și sursă pentru ciclu. Treceți cu cursorul peste o pictogramă pentru detalii." + "flags_tip": "Semnale de revizuire, anomalie și sursă pentru ciclu. Treceți cu cursorul peste o pictogramă pentru detalii.", + "cost_dynamic_tip": "Ponderat în timp: taxat la prețul în vigoare în timpul ciclului (preț efectiv {price} {cur}/kWh)." }, "stat": { "avg": "medie", @@ -1980,9 +2023,9 @@ "finished": "Ciclul a ajuns într-o stare finală și s-a încheiat." }, "store": { + "search_ph2": "Căutați după marcă sau model…", "your_model_tip": "Acesta este aparatul pe care l-ați declarat în Setări", "your_model": "Al dvs.", - "search_brand_ph": "Căutați după marcă…", "programs_count": "Programe: {n}", "browse": "Răsfoiți", "device": "Dispozitiv", @@ -2027,7 +2070,8 @@ "golden": "Reprocesare: marcarea ciclurilor de referință", "suggestions": "Reprocesare: sugestii", "ml_training": "Reprocesare: antrenare ML", - "health": "Reprocesare: sănătatea ciclurilor" + "health": "Reprocesare: sănătatea ciclurilor", + "costs": "Reprocesare: costuri de energie" }, "pg_sweep": { "optimize": "Optimizează: {param}" diff --git a/custom_components/ha_washdata/translations/panel/ru.json b/custom_components/ha_washdata/translations/panel/ru.json index 50c42339..00d8e0a8 100644 --- a/custom_components/ha_washdata/translations/panel/ru.json +++ b/custom_components/ha_washdata/translations/panel/ru.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Ближе к концу эта программа затихает примерно на {mins} мин, затем потребляет около {watts} Вт в течение {secs} с и завершается. Замечено в {seen}/{measured} измеренных циклов: прибор делает так не при каждом запуске.", + "quiet_tail": "~{mins} мин тишины в конце", "artifact_tip": "Обнаружено {n} аномалий (например, дверь открылась в середине цикла) – откройте, чтобы увидеть их на графике", "auto": "(авто-определение)", "built_in_tag": "встроенный", @@ -77,11 +79,14 @@ "approved": "Одобрено", "awaiting": "Ожидает одобрения", "imported_tip": "Импортировано из магазина сообщества. Используется только для сопоставления, не учитывается в статистике.", + "unmatchable": "не сопоставляется", "not_importable": "недоступно", "exists": "существует", - "backfilled_tip": "Обнаружено в импортированной истории мощности. Влияет только на сопоставление программ, не учитывается в статистике." + "backfilled_tip": "Обнаружено в импортированной истории мощности. Влияет только на сопоставление программ, не учитывается в статистике.", + "armed": "(применится к следующему циклу)" }, "btn": { + "reset_zoom": "Сбросить масштаб", "set_brand_model": "Указать бренд и модель", "refresh_catalog": "Обновить каталог", "add_device": "+ Добавить устройство", @@ -217,6 +222,8 @@ "trim": "Обрезать", "trim_split": "Обрезать / Разбить", "undo": "Отменить", + "unlabel_selected": "Снять метку с выбранных ({n})", + "unlabel_selected_tip": "Снять метку профиля с выбранных циклов. Они останутся в вашей истории, и их можно пометить снова позже - просто перестанут формировать этот профиль.", "use": "Использовать", "wipe_all": "Удалить все данные", "delete_selected": "Удалить выбранные ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Применить лучшее", "pg_apply_to_settings": "Сохранить в настройки", "pg_apply_to_settings_tip": "Скопировать значения, изменённые здесь, в активные настройки этого устройства", - "search": "Поиск", "import": "Импортировать", "disconnect": "Отключить", "connect_github": "Подключить GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Считать из Home Assistant", "hist_scan": "Найти циклы", "hist_import_n": "Импортировать циклы ({n})", - "hist_goto_cycles": "Показать циклы" + "hist_goto_cycles": "Показать циклы", + "save_total_cycles": "Сохранить общее количество" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Исправьте конфликты перед сохранением", "attn_title": "Конфликты настроек: {n}", + "price_entity": { + "power_sensor": "Это Датчик Мощности устройства, а не цена. Объект цены имеет приоритет над фиксированной ценой ниже, поэтому циклы считались бы по ваттам за кВт·ч - очистите это поле.", + "energy_sensor": "Это Счётчик Энергии устройства, а не цена. Объект цены имеет приоритет над фиксированной ценой ниже, поэтому циклы считались бы по показанию счётчика за кВт·ч - очистите это поле.", + "device_class": "Класс этого сенсора - {dc}, а не цена за кВт·ч. Объект цены имеет приоритет над фиксированной ценой ниже - очистите это поле или укажите сенсор тарифа.", + "unit": "Этот сенсор выдаёт значения в {unit}, а это не цена за кВт·ч. Объект цены имеет приоритет над фиксированной ценой ниже - очистите это поле или укажите сенсор тарифа." + }, "settings_banner": "Конфликты настроек: {n}. Проверьте выделенные разделы и исправьте их перед сохранением.", "settings_banner_btn": "К первому", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Источник настроек", "phase_catalog": "Каталог фаз", "playground": "Песочница", - "process_history": "История процессов", + "process_history": "Обработка истории", "settings_history": "История настроек", "status": "Статус", "status_graph": "График статуса", @@ -360,12 +373,12 @@ "wipe_history": "Очистить историю", "recommendations": "Рекомендации ({n})", "pg_detection_params": "Настройки обнаружения", - "pg_timeline": "Хронология событий", "pg_outcome": "Результат симуляции", "pg_across_cycles": "По всем вашим циклам", "community_store": "Магазин сообщества", "online_account": "Магазин сообщества и онлайн-функции", - "import_power_history": "Импорт истории мощности" + "import_power_history": "Импорт истории мощности", + "service_status": "Состояние обслуживания" }, "health": { "fair": "Приемлемое качество профиля", @@ -373,6 +386,10 @@ "poor": "⚠ Низкое качество профиля" }, "lbl": { + "envelope_position_tip": "Положение на собственной записанной кривой сопоставленной программы, {pct}%. Измеряется совмещением этого цикла с профилем, а не подсчётом времени, поэтому остаётся верным, когда запуск длиннее или короче обычного. Обновляется, пока прибор молчит, поэтому во время активной фазы может отставать.", + "envelope_position": "кривая {pct}%", + "aria_playground_chart3": "Интерактивный график мощности цикла: наведите, чтобы увидеть время/мощность, прокрутите или используйте два пальца для масштабирования, перетащите для панорамирования", + "device_unresolved": "Недоступное устройство ({id})", "drag_to_resize": "Перетащите, чтобы изменить размер", "actions": "Действия", "activity": "Активность", @@ -509,7 +526,7 @@ "profile_index": "индекс профиля →", "profile_name": "Имя профиля", "program": "Программа", - "program_tip": "Замените, какой профиль сопоставляется с текущим циклом. Авто-определение позволяет интеграции автоматически выбирать наилучшее совпадение. Закрепите конкретную программу, чтобы принудительно сопоставить её, если авто-определение ошибается или вы знаете, что запущено.", + "program_tip": "Замените, какой профиль сопоставляется с текущим циклом. Авто-определение позволяет интеграции автоматически выбирать наилучшее совпадение. Закрепите конкретную программу, чтобы принудительно сопоставить её, если авто-определение ошибается или вы знаете, что запущено. Выберите программу до запуска прибора, и она будет применена, как только начнётся следующий цикл.", "progress": "Прогресс", "raw_socket": "Необработанный сокет", "read_only": "Только чтение", @@ -544,6 +561,7 @@ "stage4": "Этап 4 – совпадение", "status": "Статус", "tail_trim": "Обрезка конца (с)", + "tap_to_dismiss": "коснитесь графика, чтобы закрыть", "timer_auto_pause": "Авто-пауза", "timer_min": "мин", "timer_msg_placeholder": "Сообщение (необязательно, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Осталось времени (модель)", "pg_time_left_model_tip": "Оценка оставшегося времени моделью (оценщик фаз + ML), а не статичный отсчёт", "phase": "Фаза", - "pg_mode_simulate": "Симуляция", "pg_mode_history": "Проверка на истории", - "pg_mode_sweep": "Перебор", "pg_ev_detected": "Обнаружено", "pg_ev_match": "Совпадение принято", "pg_ev_match_changed": "Совпадение изменено", @@ -612,14 +628,11 @@ "pg_sweep_param": "Параметр", "pg_objective": "Цель", "pg_steps": "Шаги", - "pg_sweep_2d": "2D тепловая карта", - "pg_sweep_param_y": "2-й параметр", "pg_obj_match": "Точность совпадений", "pg_obj_endtiming": "Точность завершения", "pg_obj_falseend": "Частота ложных завершений", "pg_obj_overrun": "Отклонение длительности", "pg_obj_ambiguity": "Частота неоднозначности", - "pg_best_combo": "лучшая комбинация", "pg_mode_optimize": "Оптимизировать", "pg_group_matching": "Сопоставление программ", "pg_zoom_hint": "двойной щелчок сбрасывает масштаб", @@ -706,7 +719,12 @@ "task_history_import_apply": "Импорт циклов", "evidence_real_cycles": "Циклы, выполненные этой машиной", "evidence_reference_cycles": "Загружены из магазина сообщества", - "evidence_backfill_cycles": "Найдены в импортированной истории мощности" + "evidence_backfill_cycles": "Найдены в импортированной истории мощности", + "zoom_hint_touch": "масштаб двумя пальцами", + "zoom_hint_touch_zoomed": "масштаб двумя пальцами · двойное касание для сброса", + "cycles_since_service": "{since} / {total} циклов", + "total_cycles_run": "циклов всего", + "correct_total_cycles": "Исправить общее количество" }, "log": { "all_levels": "Все уровни", @@ -756,7 +774,7 @@ "merge_cycles": "Объединить {n} циклов", "new_group": "Новая группа профилей", "new_phase": "Новая фаза", - "process_history_msg": "Повторно запустите сопоставление, обновите предложения, переобучите ML (если включено) и пересчитайте качество циклов для всех сохранённых циклов. Это может занять некоторое время.", + "process_history_msg": "Повторно запустите сопоставление, обновите предложения, переобучите ML (если включено), пересчитайте стоимость по записанным ценам на электроэнергию и пересчитайте качество циклов для всех сохранённых циклов. Это может занять некоторое время.", "process_history_ok": "Обработать", "process_history_title": "Обработка истории", "process_recording": "Обработать запись", @@ -789,6 +807,7 @@ "history_import": "Импорт истории мощности" }, "msg": { + "pg_canvas_empty3": "Выберите цикл выше и нажмите «Запустить», чтобы смоделировать его. Затем наведите указатель, чтобы увидеть значения, прокрутите или используйте два пальца для масштабирования и перетащите для панорамирования.", "tail_trim_hint": "Сколько секунд обрезать с конца", "store_sibling_hint": "Для вашей точной модели ничего не опубликовано? Очень похожая модель того же бренда обычно является хорошей отправной точкой.", "store_declare_appliance": "Укажите WashData, какое у вас устройство, и на этой вкладке появятся конфигурации, опубликованные для него другими пользователями. Можно также ввести бренд выше и просто посмотреть, что есть.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Мощность вышла за пределы обычного диапазона на ~{n}с.", "auto_label_intro": "Назначить профили не помеченным циклам, чьё совпадение превышает порог.", "automations_intro": "WashData запускает события {start} / {end} и предоставляет объекты, поэтому уведомления и действия лучше всего строить как обычные автоматизации Home Assistant. Автоматизации, использующие это устройство, отображаются ниже.", - "cleanup_intro": "Все помеченные циклы наложены. Отметьте выбросы и удалите для очистки профиля.", + "cleanup_intro": "Все помеченные циклы наложены. Отметьте выбросы, затем снимите с них метку (останутся в истории, но перестанут формировать этот профиль) или удалите их полностью.", "clear_debug_hint": "Удалите сохраненные данные отладки, чтобы освободить место.", "collecting_data": "Сбор данных. Осталось циклов до начала точной настройки: {need} ({current}/{min}).", "compare_overlay_profiles": "Наложить профили (бледные)", @@ -902,7 +921,7 @@ "preferences_adv": "Настройки", "prefs_personal": "Применяются только к вашей учётной записи Home Assistant.", "font_size_hint": "Сделайте всё на этой панели крупнее или мельче. Применяется к вашей учётной записи на этом устройстве.", - "process_history_hint": "Повторно запустите сопоставление для всех сохранённых циклов, обновите предложения по настройке, переобучите модели ML (если включено) и пересчитайте качество циклов. Запускайте после пакетного просмотра.", + "process_history_hint": "Повторно запустите сопоставление для всех сохранённых циклов, обновите предложения по настройке, переобучите модели ML (если включено), пересчитайте стоимость циклов по записанным ценам на электроэнергию и пересчитайте качество циклов. Запускайте это после серии проверок.", "profile_deleted": "Профиль удалён", "profile_poor_health_detail": "Циклы, назначенные этому профилю, имеют непоследовательную форму или низкую достоверность. Рассмотрите возможность восстановления конверта или пересмотра помеченных циклов.", "profiles_intro": "Нажмите на профиль для статистики, фаз и очистки. Группируйте почти одинаковые программы (одинаковая форма/продолжительность, разная температура или отжим), чтобы сопоставление надёжно выбирало между ними.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "У '{name}' низкая оценка соответствия - его недавние циклы сильно различаются или плохо распознаются. Просмотрите его циклы или перезапишите профиль, чтобы распознавание и оценки времени оставались точными.", "advisory_shape_drift": "'{name}' значительно отклонился от исходной формы кривой мощности. Возможно, поведение прибора со временем изменилось (например, накипь, износ). Рекомендуется перезаписать этот профиль по недавним циклам.", "advisory_shape_drift_corr": "'{name}' значительно отклонился от исходной формы кривой мощности (корреляция {corr}). Возможно, поведение прибора со временем изменилось (например, накипь, износ). Рекомендуется перезаписать этот профиль по недавним циклам.", + "advisory_unmatchable": "'{name}' никогда не будет сопоставлен: за ним нет ни одного цикла с данными мощности, поэтому WashData не может сравнить с ним выполняемый цикл. Пометьте цикл этой программы (или запишите новый), чтобы вернуть профиль в сопоставление.", + "advisory_unmatchable_no_duration": "'{name}' никогда не будет сопоставлен: у него есть цикл с данными мощности, но без пригодной длительности, поэтому WashData не с чем сравнить длину цикла при сопоставлении. Запишите эту программу заново или задайте её ожидаемую длительность, чтобы вернуть профиль в сопоставление.", "pg_no_alerts": "В этом запуске проблем не обнаружено.", "pg_override_active": "Используются ваши изменённые настройки вместо текущих", - "pg_history_intro": "Повторно прогоните последние циклы через реальный детектор и сопоставитель. Сначала измените настройку обнаружения в режиме «Симуляция», чтобы увидеть здесь сравнение до/после.", "pg_history_empty": "Нажмите «Запустить», чтобы воспроизвести циклы.", "pg_history_summary": "{detected}/{total} обнаружено · {correct} сопоставлено верно", "pg_sweep_no_metric": "Недостаточно данных для оценки этой цели.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Сканирование не удалось.", "hist_scan_expired": "Это сканирование больше недоступно. Выполните сканирование заново.", "hist_import_failed": "Импорт не удался.", - "imported_history_readonly": "Обнаружено в импортированной истории мощности. Влияет на сопоставление программ, но не учитывается в вашей статистике; такой цикл нельзя обрезать или разделить. Нажмите «Пометить», чтобы указать программу." + "imported_history_readonly": "Обнаружено в импортированной истории мощности. Влияет на сопоставление программ, но не учитывается в вашей статистике; такой цикл нельзя обрезать или разделить. Нажмите «Пометить», чтобы указать программу.", + "odometer_intro": "Это общее количество только растёт. Удаление записи о цикле его не меняет, а подсчёт продолжается и за пределами лимита хранимой истории, поэтому напоминания об обслуживании остаются верными.", + "odometer_correct_hint": "Задайте значение, если WashData записал цикл, которого не было, или если прибор уже выполнял циклы до установки WashData.", + "no_reminders_set": "Напоминания об обслуживании ещё не заданы.", + "toast_program_armed": "Программа выбрана для следующего цикла: {program}" }, "phase_desc": { "anti_crease": "Периодические короткие вращения барабана после завершения для уменьшения складок.", @@ -1161,6 +1185,10 @@ "doc": "После того как цикл превышает ожидаемую продолжительность, сколько времени посудомоечная машина должна оставаться неактивной (ниже «Порог Остановки»), прежде чем WashData прекратит ждать финальный слив и завершит цикл. Увеличьте значение, если у вашей машины длительная тихая фаза сушки перед поздним финальным сливом, который пропускается - более широкое окно позволяет изученной продолжительности следовать за сезонными изменениями (более холодная входящая вода = более длинные циклы), а не фиксироваться на старом среднем значении. Оно только сокращает ожидание относительно внутреннего 30-минутного лимита ожидания завершающего всплеска (откачки воды), но никогда не увеличивает его.", "label": "Тихое завершение пассивной сушки" }, + "anti_crease_finalize_ratio": { + "doc": "Насколько глубоко в ожидаемую длительность сопоставленной программы должен зайти цикл, прежде чем WashData сможет счесть наблюдаемое вращение барабана на малой мощности хвостом защиты от морщин и завершить цикл на этом месте. Ожидаемая длительность - это среднее значение программы, поэтому на сушильной машине, время сушки по датчику влажности у которой зависит от загрузки, большинство запусков никогда не достигает 98% этого значения, и хвост тянется до самого резервного тайм-аута - часто через 15-40 минут после того, как машина фактически остановилась. На такой сушильной машине уменьшите значение (напр., 0.75). Для стиральных машин оставьте значение по умолчанию: стиральная машина проводит большую часть цикла ниже настройки Макс. Мощность Защиты от Морщин, поэтому эта доля - единственное, что отделяет тихую фазу середины стирки от хвоста, и её снижение может сократить стирку вдвое. Оставьте пустым для значения по умолчанию (0.98). Это не то же самое, что Коэффициент умного завершения, который управляет отдельной проверкой досрочного завершения.", + "label": "Коэффициент завершения защиты от морщин" + }, "anti_wrinkle_enabled": { "doc": "Распознавайте короткие импульсы низкой мощности, которые сушильная машина излучает после основной фазы нагрева, и сохраняйте их привязанными к завершенному циклу, а не воспринимайте их как новые циклы.", "label": "Включить Защиту от Морщин" @@ -1230,7 +1258,7 @@ "label": "Количество Повторений Завершения" }, "energy_price_entity": { - "doc": "Датчик с текущей ценой на электроэнергию за кВтч (например, динамический тариф). Имеет приоритет над статической ценой, указанной ниже. Каждый цикл замораживает цену после его завершения.", + "doc": "Сенсор с текущей ценой электроэнергии за кВт·ч (например, динамический тариф). Это должна быть цена, а не собственный объект мощности или энергии розетки: он имеет приоритет над фиксированной ценой ниже, поэтому счётчик кВт·ч, указанный здесь, считает каждый цикл по показанию счётчика вместо вашего тарифа. Когда включена «Стоимость, взвешенная по времени», каждый цикл рассчитывается по цене, действовавшей в каждый момент его работы; иначе за ним закрепляется цена, действовавшая на момент завершения.", "label": "Объект Цены на Электроэнергию" }, "energy_price_static": { @@ -1258,7 +1286,7 @@ "label": "Инвертировать Внешний Триггер (срабатывать на ВЫКЛ)" }, "learning_confidence": { - "doc": "Если оценка соответствия находится между этим значением и показателем уверенности в автоматической маркировке, вам будет предложено подтвердить указанную программу в уведомлении обратной связи. Ниже этого показателя матч слишком неопределенен, чтобы его можно было увидеть. Должно быть ниже уровня достоверности автоматической маркировки.", + "doc": "Если оценка соответствия находится между этим значением и «Уверенностью Авто-Маркировки», уведомление обратной связи попросит вас подтвердить определённую программу. Ниже этой оценки соответствие слишком неопределённое, чтобы его показывать. Должно оставаться ниже «Уверенности Авто-Маркировки».", "label": "Уверенность Обучения" }, "linked_device": { @@ -1326,7 +1354,7 @@ "label": "Отправлять События HA для Уведомлений" }, "notify_icon": { - "doc": "Дополнительный значок mdi для уведомления (например, mdi:стиральная машина). Оставьте пустым для платформы по умолчанию.", + "doc": "Небольшой значок, отображаемый в уведомлении (например, mdi:washing-machine). Android рисует его в строке состояния; iOS показывает его вместо значка приложения, и для этого нужно сопутствующее приложение 2026.8 или новее. Оставьте пустым, чтобы использовать значок по умолчанию для платформы.", "label": "Иконка Уведомления" }, "notify_live_chronometer": { @@ -1402,7 +1430,7 @@ "label": "Порог Выключения Питания" }, "power_sensor": { - "doc": "Объект-датчик, сообщающий о активной мощности в ваттах для этого устройства (например, Sensor.washer_power). Все обнаружение цикла основано на этом сигнале.", + "doc": "Объект-датчик, сообщающий о активной мощности в ваттах для этого устройства (например, sensor.washer_power). Все обнаружение цикла основано на этом сигнале.", "label": "Датчик Мощности" }, "profile_duration_tolerance": { @@ -1426,7 +1454,7 @@ "label": "Порог Совпадения" }, "profile_unmatch_threshold": { - "doc": "Если результат живого матча в середине цикла опускается ниже этого значения, предварительная идентификация очищается. Держите его немного ниже порога совпадения, чтобы кратковременное снижение сходства не вернуло отображение к непревзойденному состоянию.", + "doc": "Если соответствие в реальном времени в середине цикла опускается ниже этой оценки, предварительное определение сбрасывается. Держите его немного ниже «Порога Совпадения», чтобы кратковременное снижение сходства не вернуло отображение к состоянию «нет совпадения».", "label": "Порог Несовпадения" }, "progress_reset_delay": { @@ -1437,10 +1465,6 @@ "doc": "Секунды, в течение которых насос может работать непрерывно, прежде чем он будет помечен как возможно застрявший (вызовет событие застревания насоса).", "label": "Длительность Застревания Насоса" }, - "running_dead_zone": { - "doc": "После начала цикла провалы мощности в этом окне игнорируются. Стиральные машины наполняются холодной водой (перед нагревом ее мощность падает около 0 Вт) – без этой защиты фаза наполнения выглядит как завершение цикла. При этом НЕ происходит пропуск данных: кривая полной мощности записывается от T=0. Механизм подсказок измеряет фактическую схему запуска вашей машины и автоматически определяет ее размер.", - "label": "Мёртвая Зона Работы" - }, "sampling_interval": { "doc": "Ожидаемое время между показаниями датчика – используется для определения размера окна сглаживания и правильного запуска устранения дребезга. Каждое обновление датчика фиксируется независимо от этого значения; он только калибрует последующие вычисления. Механизм подсказок измеряет фактическую частоту вращения педалей вашего датчика на основе прошлых циклов и устанавливает ее автоматически.", "label": "Интервал Выборки" @@ -1568,13 +1592,17 @@ "label": "Интервал профиля мощности", "doc": "Размер ячейки для атрибута датчика power_profile на профиль (плоский массив средних ватт по слотам), используемого внешними планировщиками, такими как EMHASS и tibber_prices. Меньшие ячейки сохраняют чёткость коротких пиков мощности; большие ячейки сглаживают форму. По умолчанию 15 мин. Только при считывании; не влияет на обнаружение." }, + "notify_live_silent": { + "label": "Тихие живые обновления", + "doc": "Только для iOS. Обновлять ход выполнения тихо: каждое обновление приходит без звука и вибрации, с пониженным приоритетом для экономии заряда, поэтому iOS может ненадолго объединять их. Первое обновление цикла (то, которое запускает Live Activity), а также уведомления о начале и завершении по-прежнему уведомляют как обычно. Выключите, чтобы получать сигнал при каждом обновлении." + }, "notify_live_sticky": { "label": "Сохранять живое уведомление при нажатии", "doc": "Только для Android. Сделать уведомление о ходе выполнения постоянным (закреплённым), чтобы нажатие не скрывало его. Выключено сохраняет поведение по умолчанию, при котором нажатие скрывает уведомление." }, "notify_live_click_action": { - "label": "Цель нажатия живого уведомления", - "doc": "Только для Android. Куда открывается нажатие на уведомление о ходе выполнения (например, /lovelace/laundry или полный URL) вместо стартовой страницы приложения. Оставьте пустым для поведения по умолчанию." + "label": "Цель нажатия уведомления", + "doc": "Куда открывается нажатие на уведомление от этого прибора, на Android и iOS. Пустое значение открывает панель WashData на этом приборе. Укажите путь к панели управления (например, /lovelace/laundry) или полный URL, чтобы перейти в другое место, либо «none», чтобы нажатие ничего не открывало." }, "door_opens_at_end": { "label": "Дверца открывается автоматически в конце", @@ -1587,6 +1615,14 @@ "profile_evidence_sources": { "label": "Циклы, формирующие программу", "doc": "Какие циклы используются для построения кривой мощности каждой программы и для сопоставления с ней завершённого цикла. Если снять галочку с какого-то вида, он перестанет влиять на ваши программы, но ничего не удалится: циклы останутся в списке Циклы, их по-прежнему можно пометить или удалить. Полезно, если вы не доверяете импортированным данным. На статистику это не влияет: в ней всегда учитываются только циклы, которые эта машина действительно выполнила. Если снять все галочки, это будет проигнорировано, ведь программа без циклов никогда не смогла бы совпасть." + }, + "energy_price_dynamic": { + "label": "Стоимость, взвешенная по времени", + "doc": "Рассчитывает стоимость каждого цикла по цене, действовавшей в каждый момент его работы, а не по единственной цене на момент завершения. Работает только с объектом цены (фиксированная цена меняться не может). Дополнительная настройка не нужна: пока цикл идёт, цена отслеживается в реальном времени, а «Обработка истории» в Диагностике пересчитывает стоимость старых циклов по данным recorder. Выключено = классическое поведение, одна цена, зафиксированная в конце." + }, + "curve_preroll_seconds": { + "doc": "Сколько секунд показаний от прерванных попыток запуска WashData может перенести в начало кривой цикла. Кривая цикла обычно начинается с той попытки запуска, которая в итоге удалась, поэтому на машине, которая делает несколько проб, прежде чем выйти на режим - выбор программы, блокировка люка, первый набор воды, - первые минуты реальной работы отсутствуют в каждой кривой. Задайте значение чуть больше самой длинной такой раскачки вашей машины (300 с с запасом хватает большинству). 0 отключает эту функцию. Учтите, что её включение сдвигает записанное начало раньше, поэтому циклы, записанные до изменения и после него, несут разную длительность для одной и той же программы; выученные средние будут смещаться, пока старые циклы не выйдут из истории.", + "label": "Предзапись кривой" } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Разбивка…", "deleting": "Удаление…", "imported": "Импортировано", - "preparing": "Подготовка…" + "preparing": "Подготовка…", + "unlabelling": "Снятие метки…" }, "tab": { "advanced": "Дополнительно", @@ -1778,6 +1815,7 @@ "recording_stopped": "Запись остановлена", "review_saved": "Проверка сохранена", "settings_conflicts": "Исправьте выделенные конфликты настроек перед сохранением.", + "settings_no_changes": "Нет изменений для сохранения", "settings_reverted": "Настройки восстановлены; интеграция перезагружается", "settings_saved": "Настройки сохранены; интеграция перезагружается", "suggestions_applied": "Предложения применены; интеграция перезагружается", @@ -1836,7 +1874,11 @@ "export_selective_done": "Экспорт скачан", "import_selective_done": "Импортировано профилей: {profiles}, циклов: {cycles}", "hist_csv_required": "Сначала загрузите файл CSV или вставьте его содержимое", - "file_read_failed": "Не удалось прочитать этот файл" + "file_read_failed": "Не удалось прочитать этот файл", + "unlabel_done": "Метка снята с {count} цикла(ов)", + "unlabel_failed": "Не удалось снять метку: {error}", + "odometer_saved": "Общее количество циклов сохранено", + "odometer_save_failed": "Не удалось сохранить общее количество: {error}" }, "suggestion": { "both_agree": "WashData рекомендует", @@ -1883,10 +1925,6 @@ "higher": "Подтверждает только высокоуверенные совпадения; больше циклов остаётся без метки", "lower": "Сопоставляет больше циклов; некоторые могут быть немного неверны" }, - "running_dead_zone": { - "higher": "Игнорирует кратковременные скачки мощности в простое – менее чувствителен к шуму", - "lower": "Реагирует на более короткие всплески мощности – улавливает быструю переходную активность" - }, "start_threshold_w": { "higher": "Исключает ложные запуски от кратковременных скачков мощности", "lower": "Улавливает маломощные программы; может сработать на шум" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} корректных циклов ({pct}%) имели внутреннюю паузу >60s с возобновлением; требуется {val} подтверждение(й) завершения.", "learning_confidence": "Уверенность p05 для {cycles} циклов, размеченных пользователем ({p05}); ниже этого значения запрашивается проверка.", "match_interval": "На основе наблюдаемой частоты обновлений (медиана={median}s) * 10.", + "match_interval_capped": "Ограничено так, чтобы {persistence} последовательных совпадений уместились в {pct}% самой короткой программы ({shortest}s); одна лишь частота обновлений (медиана={median}s) подсказала бы более длинный интервал.", + "match_interval_floored": "Одна только самая короткая программа ({shortest}s) ограничила бы это значение до {cap}s, что ниже минимума в {minimum}s, поэтому оно удерживается на минимуме: серия из {persistence} последовательных совпадений занимает {budget}s, то есть больше {pct}% этой программы. Сопоставление выполняется только при поступлении нового показания (медиана={median}s), поэтому минимум не даёт лишней нагрузки.", "max_duration_ratio": "На основе длительностей размеченных циклов (p95={p95}).", "min_active": "На основе минимальной активной мощности ({min}W), зафиксированной в последнем цикле.", "min_duration_ratio": "Оставлено как можно ниже, чтобы программа распознавалась в начале цикла; пороги уверенности и неоднозначности предотвращают преждевременные подтверждения.", @@ -1973,6 +2013,7 @@ "start_duration_threshold": "Секунды выше порога для подтверждения старта", "start_threshold_w": "Минимум ватт, чтобы считать запущенным", "stop_threshold_w": "Ниже этого машина считается выключенной", + "anti_crease_finalize_ratio": "Доля ожидаемой длительности сопоставленной программы, которую должен достичь цикл, прежде чем можно завершить хвост вращения защиты от морщин; уменьшите её на сушильных машинах с длинным хвостом вращения, для стиральных машин оставьте значение по умолчанию", "anti_wrinkle_enabled": "Поглощать импульсы вращения барабана после основной фазы, а не считать их новыми циклами", "anti_wrinkle_exit_power": "Мощность между импульсами должна падать ниже этого, чтобы защита от морщин оставалась активной", "anti_wrinkle_idle_timeout": "Допустимая тишина между двумя импульсами вращения барабана, прежде чем защита от морщин завершится", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Этап 3: расстояние полунасыщения DDTW; меньше = чувствительнее к форме (по умолчанию 30)", "dtw_refine_top_n": "Этап 3: кандидаты, переоцениваемые DTW; повысьте до 7-9, если правильный профиль занимает 4-5 место (по умолчанию 5)", "duration_scale": "Этап 4: логарифмическое отношение, при котором совпадение по длительности уменьшается вдвое; меньше = более строгий штраф (по умолчанию 0,175)", - "energy_scale": "Этап 4: логарифмическое отношение, при котором совпадение по энергии уменьшается вдвое; меньше = более строгий штраф (по умолчанию 0,25)" + "energy_scale": "Этап 4: логарифмическое отношение, при котором совпадение по энергии уменьшается вдвое; меньше = более строгий штраф (по умолчанию 0,25)", + "curve_preroll_seconds": "Секунды показаний от прерванных попыток запуска, переносимые в начало кривой цикла; 0 = выключено" }, "col": { "profile_tip": "Название сопоставленной программы. «Без метки» означает, что в конце цикла не совпал ни один профиль.", @@ -1998,10 +2040,11 @@ "date_tip": "Дата и время начала цикла.", "duration_tip": "Общее время работы цикла от начала до конца.", "energy_tip": "Общее потреблённое количество энергии (кВт·ч). Вычисляется интегрированием мощности по времени.", - "cost_tip": "Стоимость энергии для этого цикла, зафиксированная при завершении по действовавшей тогда цене (энергия × цена за кВт·ч). Укажите цену в Настройках, чтобы заполнить это поле.", + "cost_tip": "Стоимость энергии для этого цикла, зафиксированная при завершении. Когда включена «Стоимость, взвешенная по времени», кривая мощности рассчитывается по цене, действующей в каждый момент; иначе энергия x единственная цена, действующая в конце. Задайте цену в Настройках, чтобы значение появилось.", "confidence_tip": "Достоверность совпадения профиля (0–100%). Насколько точно кривая мощности цикла совпала с определённой программой.", "health_tip": "ML-качество цикла (выше = лучше). Нажмите на цикл, чтобы проверить и оценить его.", - "flags_tip": "Метки сообщества и индикаторы качества данных" + "flags_tip": "Метки сообщества и индикаторы качества данных", + "cost_dynamic_tip": "Взвешено по времени: рассчитано по цене, действовавшей во время цикла (эффективно {price} {cur}/кВт·ч)." }, "stat": { "avg": "сред.", @@ -2028,9 +2071,9 @@ "finished": "Цикл достиг конечного состояния и завершился." }, "store": { + "search_ph2": "Поиск по бренду или модели…", "your_model_tip": "Это устройство, которое вы указали в настройках", "your_model": "Ваше", - "search_brand_ph": "Поиск по бренду…", "programs_count": "Программы: {n}", "browse": "Обзор", "device": "Устройство", @@ -2075,7 +2118,8 @@ "golden": "Повторная обработка: заполнение эталонных", "suggestions": "Повторная обработка: предложения", "ml_training": "Повторная обработка: обучение ML", - "health": "Повторная обработка: качество циклов" + "health": "Повторная обработка: качество циклов", + "costs": "Повторная обработка: затраты на энергию" }, "pg_sweep": { "optimize": "Оптимизация: {param}" diff --git a/custom_components/ha_washdata/translations/panel/sk.json b/custom_components/ha_washdata/translations/panel/sk.json index 285b8c54..a05840a7 100644 --- a/custom_components/ha_washdata/translations/panel/sk.json +++ b/custom_components/ha_washdata/translations/panel/sk.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Ku koncu tento program asi na {mins} min stíchne a pred dokončením odoberá približne {watts} W počas {secs} s. Zaznamenané pri {seen} z {measured} zmeraných cyklov: spotrebič to nerobí pri každom behu.", + "quiet_tail": "tichý koniec ~{mins} min", "artifact_tip": "Zistilo sa {n} anomálií (napr. dvere otvorené uprostred cyklu) – otvorte ich, aby ste ich videli na grafe", "auto": "(automaticky zistené)", "built_in_tag": "vstavaný", @@ -77,11 +79,14 @@ "approved": "Schválené", "awaiting": "Čaká na schválenie", "imported_tip": "Importované z komunitného obchodu. Používa sa iba na porovnávanie, nezapočítava sa do štatistík.", + "unmatchable": "nedá sa rozpoznať", "not_importable": "nedostupné", "exists": "existuje", - "backfilled_tip": "Detekované v importovanej histórii výkonu. Ovplyvňuje iba priraďovanie programov, nezapočítava sa do štatistík." + "backfilled_tip": "Detekované v importovanej histórii výkonu. Ovplyvňuje iba priraďovanie programov, nezapočítava sa do štatistík.", + "armed": "(použije sa na ďalší cyklus)" }, "btn": { + "reset_zoom": "Obnoviť priblíženie", "set_brand_model": "Nastaviť značku a model", "refresh_catalog": "Obnoviť katalóg", "add_device": "+ Pridať zariadenie", @@ -217,6 +222,8 @@ "trim": "Orezať", "trim_split": "Orezať / Rozdeliť", "undo": "Späť", + "unlabel_selected": "Odobrať štítok vybraným ({n})", + "unlabel_selected_tip": "Odstráni z vybraných cyklov štítok profilu. Zostanú vo vašej histórii a možno ich neskôr znova označiť - len prestanú tvoriť tento profil.", "use": "Použiť", "wipe_all": "Vymazať všetky dáta", "delete_selected": "Odstrániť vybrané ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Použiť najlepšie", "pg_apply_to_settings": "Uložiť do nastavení", "pg_apply_to_settings_tip": "Skopírovať hodnoty, ktoré ste tu upravili, do aktívnych nastavení tohto zariadenia", - "search": "Hľadať", "import": "Importovať", "disconnect": "Odpojiť", "connect_github": "Pripojiť GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Načítať z Home Assistant", "hist_scan": "Vyhľadať cykly", "hist_import_n": "Importovať cykly: {n}", - "hist_goto_cycles": "Zobraziť cykly" + "hist_goto_cycles": "Zobraziť cykly", + "save_total_cycles": "Uložiť celkový počet" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Musí byť nad Prahom vypnutia napájania ({pot} W)", "threshold": "Musí byť pod Prahom zastavenia ({stop} W), aby mal účinok" }, + "price_entity": { + "power_sensor": "Toto je Senzor výkonu zariadenia, nie cena. Entita ceny má prednosť pred statickou cenou nižšie, takže by sa cykly účtovali vo wattoch za kWh - vymažte toto pole.", + "energy_sensor": "Toto je Merač energie zariadenia, nie cena. Entita ceny má prednosť pred statickou cenou nižšie, takže by sa cykly účtovali podľa stavu merača za kWh - vymažte toto pole.", + "device_class": "Trieda tohto senzora je {dc}, nie cena za kWh. Entita ceny má prednosť pred statickou cenou nižšie - vymažte toto pole alebo ho nasmerujte na senzor tarify.", + "unit": "Tento senzor meria v {unit}, čo nie je cena za kWh. Entita ceny má prednosť pred statickou cenou nižšie - vymažte toto pole alebo ho nasmerujte na senzor tarify." + }, "pump_stuck": { "duration": "Musí byť menší ako Časový limit bez aktualizácie ({to} s), aby sa alarm zaseknutia spustil pred ukončením cyklu watchdogom", "timeout": "Musí presiahnuť Trvanie zaseknutia čerpadla ({ps} s), aby sa alarm spustil pred vynúteným zastavením cyklu" @@ -349,7 +362,7 @@ "pg_settings_source": "Zdroj nastavení", "phase_catalog": "Katalóg fáz", "playground": "Testovacie prostredie", - "process_history": "História procesov", + "process_history": "Spracovanie histórie", "settings_history": "História nastavení", "status": "Stav", "status_graph": "Graf stavu", @@ -360,12 +373,12 @@ "wipe_history": "Vymazať históriu", "recommendations": "Odporúčania ({n})", "pg_detection_params": "Nastavenia detekcie", - "pg_timeline": "Časová os udalostí", "pg_outcome": "Výsledok simulácie", "pg_across_cycles": "Naprieč vašimi cyklami", "community_store": "Komunitný obchod", "online_account": "Komunitný obchod a online funkcie", - "import_power_history": "Import histórie výkonu" + "import_power_history": "Import histórie výkonu", + "service_status": "Stav servisu" }, "health": { "fair": "Prijateľná kvalita profilu", @@ -373,6 +386,10 @@ "poor": "⚠ Zlá kvalita profilu" }, "lbl": { + "envelope_position_tip": "Poloha na vlastnej zaznamenanej krivke zhodného programu, {pct}%. Určuje sa porovnaním tvaru tohto cyklu s profilom, nie odpočítavaním času, takže zostáva správna aj pri dlhšom alebo kratšom behu. Obnovuje sa, keď je spotrebič v pokoji, takže počas aktívnej fázy môže zaostávať.", + "envelope_position": "krivka {pct}%", + "aria_playground_chart3": "Interaktívny graf výkonu cyklu: prejdením myšou zobrazíte čas/výkon, kolieskom alebo dvoma prstami priblížite, ťahaním posuniete", + "device_unresolved": "Nedostupné zariadenie ({id})", "drag_to_resize": "Potiahnutím zmeníte veľkosť", "actions": "Akcie", "activity": "Aktivita", @@ -509,7 +526,7 @@ "profile_index": "index profilu →", "profile_name": "Názov profilu", "program": "Program", - "program_tip": "Prepíšte profil priradený k aktuálnemu cyklu. Automatická detekcia umožňuje integrácii automaticky vybrať najlepšiu zhodu. Pripnite konkrétny program na vynútenie zhody, keď automatická detekcia nie je správna alebo viete, čo beží.", + "program_tip": "Prepíšte profil priradený k aktuálnemu cyklu. Automatická detekcia umožňuje integrácii automaticky vybrať najlepšiu zhodu. Pripnite konkrétny program na vynútenie zhody, keď automatická detekcia nie je správna alebo viete, čo beží. Vyberte program pred spustením spotrebiča a použije sa hneď, ako začne ďalší cyklus.", "progress": "Priebeh", "raw_socket": "Surový socket", "read_only": "Len na čítanie", @@ -544,6 +561,7 @@ "stage4": "Etapa 4 – zhoda", "status": "Stav", "tail_trim": "Orezanie konca (s)", + "tap_to_dismiss": "ťuknutím na graf zatvoríte", "timer_auto_pause": "Automatické pozastavenie", "timer_min": "min", "timer_msg_placeholder": "Správa (voliteľné, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Zostávajúci čas (model)", "pg_time_left_model_tip": "Modelom odhadovaný zostávajúci čas (odhad fázy + kombinácia s ML), nie statické odpočítavanie", "phase": "Fáza", - "pg_mode_simulate": "Simulovať", "pg_mode_history": "Otestovať na histórii", - "pg_mode_sweep": "Prehľadávanie", "pg_ev_detected": "Zistené", "pg_ev_match": "Zhoda potvrdená", "pg_ev_match_changed": "Zhoda zmenená", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Cieľ", "pg_steps": "Kroky", - "pg_sweep_2d": "Teplotná mapa 2D", - "pg_sweep_param_y": "2. parameter", "pg_obj_match": "Presnosť zhody", "pg_obj_endtiming": "Presnosť konca", "pg_obj_falseend": "Miera falošných koncov", "pg_obj_overrun": "Odchýlka trvania", "pg_obj_ambiguity": "Miera nejednoznačnosti", - "pg_best_combo": "najlepšia kombinácia", "pg_mode_optimize": "Optimalizovať", "pg_group_matching": "Priraďovanie programov", "pg_zoom_hint": "dvojitým kliknutím obnovíte priblíženie", @@ -706,7 +719,12 @@ "task_history_import_apply": "Import cyklov", "evidence_real_cycles": "Cykly, ktoré tento spotrebič vykonal", "evidence_reference_cycles": "Stiahnuté z komunitného obchodu", - "evidence_backfill_cycles": "Nájdené v importovanej histórii výkonu" + "evidence_backfill_cycles": "Nájdené v importovanej histórii výkonu", + "zoom_hint_touch": "dvoma prstami priblížite", + "zoom_hint_touch_zoomed": "dvoma prstami priblížite · dvojitým ťuknutím obnovíte", + "cycles_since_service": "{since} / {total} cyklov", + "total_cycles_run": "cyklov celkom", + "correct_total_cycles": "Opraviť celkový počet" }, "log": { "all_levels": "Všetky úrovne", @@ -756,7 +774,7 @@ "merge_cycles": "Zlúčiť {n} cyklov", "new_group": "Nová skupina profilov", "new_phase": "Nová fáza", - "process_history_msg": "Znova spustite párovanie, obnovte návrhy, preškolte ML (ak je povolené) a prepočítajte zdravie cyklov pre všetky uložené cykly. Môže to chvíľu trvať.", + "process_history_msg": "Znova spustite vyhodnotenie zhody, obnovte návrhy, preškolte ML (ak je povolené), prepočítajte náklady podľa zaznamenaných cien energie a prepočítajte zdravie cyklov pre všetky uložené cykly. Môže to chvíľu trvať.", "process_history_ok": "Spracovať", "process_history_title": "Spracovanie histórie", "process_recording": "Spracovať nahrávku", @@ -789,6 +807,7 @@ "history_import": "Import histórie výkonu" }, "msg": { + "pg_canvas_empty3": "Vyberte cyklus vyššie a stlačením Spustiť ho nasimulujte. Potom prejdením myšou zobrazíte hodnoty, kolieskom alebo dvoma prstami priblížite a ťahaním posuniete.", "tail_trim_hint": "Koľko sekúnd odstrániť z konca", "store_sibling_hint": "Pre váš konkrétny model nie je nič zdieľané? Veľmi podobný model od tej istej značky je zvyčajne dobrý východiskový bod.", "store_declare_appliance": "Povedzte WashData, aký spotrebič máte, a táto karta zobrazí nastavenia, ktoré pre neho zdieľali ostatní používatelia. Môžete tiež zadať značku vyššie a poobzerať sa.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Bol nad bežným pásmom výkonu počas ~{n} s.", "auto_label_intro": "Priradiť profily neoznačeným cyklom, ktorých zhoda prekračuje prah.", "automations_intro": "WashData spúšťa udalosti {start} / {end} a sprístupňuje entity, takže notifikácie a akcie sa najlepšie vytvárajú ako bežné automatizácie Home Assistant. Automatizácie používajúce toto zariadenie sú zobrazené nižšie.", - "cleanup_intro": "Všetky označené cykly naložené. Označte odchýlky a odstráňte pre vyčistenie profilu.", + "cleanup_intro": "Všetky označené cykly navrstvené na seba. Zaškrtnite odchýlené cykly a potom im odoberte štítok (zostanú v histórii, ale už netvoria tento profil), alebo ich rovno odstráňte.", "clear_debug_hint": "Odstráňte uložené údaje ladenia, aby ste uvoľnili miesto.", "collecting_data": "Zbieranie dát. Zostávajúce cykly do spustenia doladenia: {need} ({current}/{min}).", "compare_overlay_profiles": "Prekryté profily (bledé)", @@ -902,7 +921,7 @@ "preferences_adv": "Predvoľby", "prefs_personal": "Platí iba pre váš účet Home Assistant.", "font_size_hint": "Zväčšite alebo zmenšite všetko v tomto paneli. Platí pre váš účet na tomto zariadení.", - "process_history_hint": "Znova spustite párovanie pre všetky uložené cykly, obnovte návrhy ladenia, preškolte modely ML (ak sú povolené) a prepočítajte zdravie cyklov. Spustite po sérii recenzií.", + "process_history_hint": "Znova spustite vyhodnotenie zhody pre všetky uložené cykly, obnovte návrhy ladenia, preškolte modely ML (ak sú povolené), prepočítajte náklady cyklov podľa zaznamenaných cien energie a prepočítajte zdravie cyklov. Spustite to po sérii kontrol.", "profile_deleted": "Profil odstránený", "profile_poor_health_detail": "Cykly priradené k tomuto profilu majú nekonzistentný tvar alebo nízku spoľahlivosť. Zvážte prestavbu obálky alebo kontrolu cyklov s označením.", "profiles_intro": "Kliknite na profil pre štatistiky, fázy a vyčistenie. Zoskupte takmer identické programy (rovnaký tvar/trvanie, rôzna teplota alebo odstreďovanie), aby zhoda spoľahlivo vyberala medzi nimi.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "'{name}' má nízke skóre zhody - jeho posledné cykly sa veľmi líšia alebo sa slabo rozpoznávajú. Prezrite jeho cykly alebo profil nahrajte znova, aby rozpoznávanie a odhady času zostali presné.", "advisory_shape_drift": "'{name}' sa výrazne odchýlil od pôvodného tvaru krivky výkonu. Správanie spotrebiča sa mohlo časom zmeniť (napr. vodný kameň, opotrebenie). Zvážte opätovné nahranie tohto profilu z posledných cyklov.", "advisory_shape_drift_corr": "'{name}' sa výrazne odchýlil od pôvodného tvaru krivky výkonu (korelácia {corr}). Správanie spotrebiča sa mohlo časom zmeniť (napr. vodný kameň, opotrebenie). Zvážte opätovné nahranie tohto profilu z posledných cyklov.", + "advisory_unmatchable": "'{name}' nie je možné nikdy rozpoznať: nestojí za ním žiadny cyklus s údajmi o výkone, takže WashData nemá s čím porovnať prebiehajúci cyklus. Označte týmto programom niektorý cyklus (alebo jeden zaznamenajte), aby sa profil vrátil do rozpoznávania.", + "advisory_unmatchable_no_duration": "'{name}' nie je možné nikdy rozpoznať: má cyklus s údajmi o výkone, ale bez použiteľného času trvania, takže WashData nemá dĺžku, podľa ktorej by sa dala zhoda posúdiť. Zaznamenajte tento program znova alebo nastavte jeho očakávaný čas trvania, aby sa vrátil do rozpoznávania.", "pg_no_alerts": "V tomto behu neboli zistené žiadne problémy.", "pg_override_active": "Používajú sa vaše upravené nastavenia oproti aktuálnym", - "pg_history_intro": "Prehrajte svoje nedávne cykly cez skutočný detektor a priraďovač. Najprv upravte nastavenie detekcie v režime Simulovať, aby ste tu videli porovnanie pred/po.", "pg_history_empty": "Stlačte Spustiť na prehratie vašich cyklov.", "pg_history_summary": "{detected}/{total} zistených · {correct} správne priradených", "pg_sweep_no_metric": "Nedostatok dát na vyhodnotenie tohto cieľa.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Vyhľadávanie sa nepodarilo.", "hist_scan_expired": "Toto vyhľadávanie už nie je dostupné. Spustite ho prosím znova.", "hist_import_failed": "Import sa nepodaril.", - "imported_history_readonly": "Detekované v importovanej histórii výkonu. Ovplyvňuje priraďovanie programov, ale nezapočítava sa do vašich štatistík a nedá sa orezať ani rozdeliť. Označením mu priraďte program." + "imported_history_readonly": "Detekované v importovanej histórii výkonu. Ovplyvňuje priraďovanie programov, ale nezapočítava sa do vašich štatistík a nedá sa orezať ani rozdeliť. Označením mu priraďte program.", + "odometer_intro": "Tento celkový počet iba rastie. Odstránenie záznamu o cykle ho nezmení a počíta ďalej aj za limitom uloženej histórie, takže pripomienky servisu zostávajú správne.", + "odometer_correct_hint": "Nastavte, ak WashData zaznamenalo cyklus, ktorý nikdy neprebehol, alebo ak spotrebič už absolvoval cykly pred inštaláciou WashData.", + "no_reminders_set": "Zatiaľ nie sú nastavené žiadne pripomienky servisu.", + "toast_program_armed": "Program nastavený pre ďalší cyklus: {program}" }, "phase_desc": { "anti_crease": "Príležitostné krátke vírenia po dokončení na predchádzanie krčeniu oblečenia.", @@ -1161,6 +1185,10 @@ "label": "Uvoľnenie po tichej fáze sušenia", "doc": "Keď cyklus prekročí svoje očakávané trvanie, ako dlho musí umývačka zostať v pokoji (pod Prahom zastavenia), než WashData prestane čakať na záverečné odčerpanie vody a ukončí cyklus. Zvýšte hodnotu, ak má váš spotrebič dlhú tichú fázu sušenia pred neskorým záverečným odčerpaním, ktoré uniká detekcii - širšie okno umožňuje, aby naučené trvanie sledovalo sezónny posun (studenšia privádzaná voda = dlhšie cykly) namiesto toho, aby zostalo uzamknuté na starom priemere. Čakanie iba skracuje vzhľadom na interný 30-minútový limit čakania na záverečné odčerpanie, nikdy ho nepredlžuje." }, + "anti_crease_finalize_ratio": { + "doc": "Ako ďaleko v očakávanej dobe trvania priradeného programu musí byť cyklus, než smie WashData vyhodnotiť pozorované otáčanie bubna s nízkym príkonom ako chvost ochrany pred krčením a cyklus tam ukončiť. Očakávaná doba trvania je priemer programu, takže pri sušičke, ktorej doba senzorového sušenia sa riadi náplňou, väčšina behov nikdy nedosiahne 98 % tejto hodnoty a chvost zostane visieť až do záložného časového limitu - často 15 až 40 minút potom, čo sa stroj skutočne zastavil. Pri takejto sušičke hodnotu znížte (napr. 0,75). Pri práčkach ponechajte predvolenú hodnotu: práčka strávi väčšinu cyklu pod hodnotou Max. výkon ochrany pred krčením, takže tento zlomok je jediné, čo oddeľuje tichú fázu uprostred prania od chvosta, a jeho zníženie môže pranie skrátiť na polovicu. Ponechajte prázdne pre predvolenú hodnotu (0,98). Je to iné nastavenie než Pomer inteligentného ukončenia, ktoré riadi samostatnú kontrolu skoršieho ukončenia.", + "label": "Pomer dokončenia ochrany pred krčením" + }, "anti_wrinkle_enabled": { "doc": "Rozpoznajte krátke pulzy bubna s nízkym výkonom, ktoré sušička vydáva po hlavnej fáze ohrevu, a nechajte ich pripojené k dokončenému cyklu namiesto toho, aby ste ich čítali ako nové cykly.", "label": "Povoliť detekciu ochrany pred krčením" @@ -1182,7 +1210,7 @@ "label": "Max. výkon ochrany pred krčením" }, "auto_label_confidence": { - "doc": "Ak je skóre zápasu na konci cyklu rovnaké alebo vyššie, program sa označí automaticky bez výzvy na potvrdenie. Zvýšte ho tak, aby vyžadoval vyššiu istotu pred automatickým označovaním; znížte ho, aby ste automatizovali viac. Funguje v spojení s Learning Confidence pod ním.", + "doc": "Ak je skóre zhody na konci cyklu rovnaké alebo vyššie ako toto, program sa označí automaticky bez výzvy na potvrdenie. Zvýšte ho, ak chcete pred automatickým označovaním vyžadovať vyššiu istotu; znížte ho, ak chcete automatizovať viac. Funguje v spojení so Spoľahlivosťou učenia pod ním.", "label": "Spoľahlivosť autooznačovania" }, "auto_maintenance": { @@ -1230,7 +1258,7 @@ "label": "Počet opakovaní ukončenia" }, "energy_price_entity": { - "doc": "Senzor s aktuálnou cenou elektriny za kWh (napr. dynamická tarifa). Má prednosť pred statickou cenou nižšie. Každý cyklus zmrazí cenu platnú po jeho skončení.", + "doc": "Senzor s aktuálnou cenou elektriny za kWh (napr. dynamická tarifa). Musí to byť cena, nie vlastná entita výkonu alebo energie zásuvky: má prednosť pred statickou cenou nižšie, takže počítadlo kWh zadané tu účtuje každý cyklus podľa stavu merača namiesto vašej tarify. Keď sú Časovo vážené náklady zapnuté, každý cyklus sa účtuje cenou platnou v každom okamihu jeho behu; inak sa naň zafixuje cena platná pri jeho dokončení.", "label": "Entita ceny energie" }, "energy_price_static": { @@ -1258,7 +1286,7 @@ "label": "Invertovať externý spúšť (spustiť pri VYPNUTÍ)" }, "learning_confidence": { - "doc": "Ak je skóre zhody medzi týmto a Auto-Label Confidence, upozornenie na spätnú väzbu vás požiada o overenie identifikovaného programu. Pod týmto skóre je zápas príliš neistý. Musí sa udržiavať pod úrovňou spoľahlivosti automatického označenia.", + "doc": "Ak skóre zhody padne medzi toto a Spoľahlivosť autooznačovania, upozornenie na spätnú väzbu vás požiada o overenie identifikovaného programu. Pod týmto skóre je zhoda príliš neistá na to, aby sa zobrazila. Musí sa udržiavať pod Spoľahlivosťou autooznačovania.", "label": "Spoľahlivosť učenia" }, "linked_device": { @@ -1326,7 +1354,7 @@ "label": "Spúšťať HA udalosti pre upozornenia" }, "notify_icon": { - "doc": "Voliteľná ikona mdi pre upozornenie (napr. mdi:pračka). Pre predvolenú platformu nechajte prázdne.", + "doc": "Malá ikona zobrazená v upozornení (napr. mdi:washing-machine). Android ju vykreslí v stavovom riadku; iOS ju zobrazí namiesto ikony aplikácie a vyžaduje mobilnú aplikáciu Home Assistant (companion app) 2026.8 alebo novšiu. Nechajte prázdne pre predvolenú ikonu platformy.", "label": "Ikona upozornenia" }, "notify_live_chronometer": { @@ -1426,7 +1454,7 @@ "label": "Prah zhody" }, "profile_unmatch_threshold": { - "doc": "Ak živý zápas v strede cyklu klesne pod toto skóre, predbežná identifikácia sa vymaže. Udržujte ho trochu pod prahom zhody, aby krátky pokles podobnosti neprevrátil displej späť na neporovnateľný.", + "doc": "Ak živá zhoda v strede cyklu klesne pod toto skóre, predbežná identifikácia sa vymaže. Udržujte ho trochu pod Prahom zhody, aby krátky pokles podobnosti neprepol zobrazenie späť na stav bez zhody.", "label": "Prah nezhody" }, "progress_reset_delay": { @@ -1437,10 +1465,6 @@ "doc": "Sekundy môže čerpadlo bežať nepretržite predtým, ako je označené ako možné zablokovanie (spustí udalosť zaseknutého čerpadla).", "label": "Trvanie zaseknutia čerpadla" }, - "running_dead_zone": { - "doc": "Po spustení cyklu sa poklesy výkonu v rámci tohto okna ignorujú. Práčky sa plnia studenou vodou (pred ohrevom poklesne takmer 0 W) - bez tejto ochrany vyzerá fáza plnenia ako koniec cyklu. Týmto sa nepreskakujú údaje: celý priebeh výkonu sa zaznamenáva od T=0. Nástroj návrhov meria skutočný vzor spustenia vášho počítača a automaticky ho upravuje.", - "label": "Mŕtva zóna behu" - }, "sampling_interval": { "doc": "Očakávaný čas medzi snímaniami snímača – používa sa na nastavenie veľkosti vyhladzovacieho okna a správne spustenie odrazu. Každá aktualizácia snímača sa zaznamená bez ohľadu na túto hodnotu; kalibruje iba následné výpočty. Nástroj návrhov meria skutočnú kadenciu vášho senzora z minulých cyklov a automaticky ju nastavuje.", "label": "Interval vzorkovania" @@ -1568,13 +1592,17 @@ "label": "Interval výkonového profilu", "doc": "Veľkosť segmentu pre atribút senzora power_profile pre daný profil (plochá sada priemerných wattov za slot spotrebovaná externými plánovačmi ako EMHASS a tibber_prices). Menšie segmenty zachovávajú krátke výkonové špičky ostré; väčšie segmenty vyhladzujú tvar. Predvolene 15 min. Iba pri načítaní; neovplyvňuje detekciu." }, + "notify_live_silent": { + "label": "Tiché živé aktualizácie", + "doc": "Iba iOS. Aktualizovať živý priebeh potichu: každá aktualizácia príde bez zvuku a vibrácií, s nižšou prioritou šetriacou batériu, takže ich iOS môže na chvíľu zoskupiť. Prvá aktualizácia cyklu (tá, ktorá spustí Live Activity) a oznámenia o štarte a dokončení upozornia ako zvyčajne. Vypnutím budete upozornení na každú aktualizáciu." + }, "notify_live_sticky": { "label": "Zachovať živé oznámenie po klepnutí", "doc": "Iba Android. Živé oznámenie priebehu nastavte ako trvalé (prilepené), aby jeho klepnutím nedošlo k zavretiu prebiehajúceho vlákna. Vypnuté zachováva predvolené správanie, kde klepnutie oznámenie zavrie." }, "notify_live_click_action": { - "label": "Cieľ klepnutia na živé oznámenie", - "doc": "Iba Android. Kam klepnutie na živé oznámenie priebehu otvorí (napr. /lovelace/laundry alebo celá URL) namiesto vstupnej stránky aplikácie. Ponechajte prázdne pre predvolené nastavenie." + "label": "Cieľ klepnutia na oznámenie", + "doc": "Kam vedie klepnutie na oznámenie z tohto spotrebiča, v systémoch Android aj iOS. Prázdne pole otvorí panel WashData na tomto spotrebiči. Zadajte cestu k nástenke (napr. /lovelace/laundry) alebo celú URL, ak má viesť inam, prípadne „none“, ak klepnutie nemá otvárať nič." }, "door_opens_at_end": { "label": "Dvere sa na konci automaticky otvoria", @@ -1587,6 +1615,14 @@ "profile_evidence_sources": { "label": "Cykly tvoriace program", "doc": "Ktoré cykly sa používajú na zostavenie krivky výkonu každého programu a na priradenie dokončeného cyklu k nej. Keď niektorý druh odškrtnete, prestane ovplyvňovať vaše programy, ale nič sa nezmaže - cykly zostanú v zozname Cykly a stále ich možno označiť alebo odstrániť. Užitočné, ak nedôverujete importovaným údajom. Štatistiky to neovplyvní: vždy počítajú len cykly, ktoré tento spotrebič skutočne vykonal. Odškrtnutie všetkých možností sa ignoruje, pretože program bez cyklov by nikdy nemohol byť priradený." + }, + "energy_price_dynamic": { + "label": "Časovo vážené náklady", + "doc": "Účtuje každý cyklus cenou platnou v každom okamihu jeho behu namiesto jedinej ceny platnej pri jeho dokončení. Týka sa len entity ceny (statická cena sa meniť nemôže). Nevyžaduje žiadne ďalšie nastavenie: cena sa počas cyklu sleduje naživo a Spracovanie histórie v Diagnostike prepočíta náklady starších cyklov z recordera. Vypnuté = klasické správanie, jedna cena zafixovaná na konci." + }, + "curve_preroll_seconds": { + "doc": "Koľko sekúnd meraní z neúspešných pokusov o spustenie môže WashData preniesť na začiatok krivky cyklu. Krivka cyklu obvykle začína pri pokuse o spustenie, ktorý nakoniec uspel, takže pri stroji, ktorý to pred rozbehnutím niekoľkokrát skúsi - voľba programu, zámok dverí, prvé napúšťanie - chýbajú v každej krivke prvé minúty skutočnej činnosti. Nastavte túto hodnotu o niečo vyššie, než je najdlhší takýto rozbeh vášho stroja (300 s väčšine bohato stačí). 0 funkciu vypne. Majte na pamäti, že zapnutie posunie zaznamenaný začiatok skôr, takže cykly zaznamenané pred zmenou a po nej nesú pre ten istý program rozdielne doby trvania; naučené priemery sa budú posúvať, kým staršie cykly nevypadnú z histórie.", + "label": "Nábeh krivky" } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Rozdeľovanie…", "deleting": "Odstraňovanie…", "imported": "Importované", - "preparing": "Príprava…" + "preparing": "Príprava…", + "unlabelling": "Odoberanie štítka…" }, "suggestion": { "both_agree": "WashData odporúča", @@ -1745,10 +1782,6 @@ "higher": "Potvrdzuje iba vysoko isté zhody; viac cyklov zostane neoznačených", "lower": "Páruje viac cyklov; niektoré môžu byť mierne nesprávne" }, - "running_dead_zone": { - "higher": "Ignoruje krátke výkonové špičky počas nečinnosti – menej citlivý na rušenie", - "lower": "Reaguje na kratšie výkonové impulzy – zachytí rýchlu prechodnú aktivitu" - }, "start_threshold_w": { "higher": "Zabraňuje falošným štartom z krátkych výkonových špiček", "lower": "Zachytí programy s nízkym príkonom; môže reagovať na šum" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} čistých cyklov ({pct}%) malo vnútornú pauzu >60s, ktorá pokračovala; vyžadovať {val} potvrdení konca.", "learning_confidence": "Istota p05 z {cycles} cyklov označených používateľom ({p05}); pod touto hodnotou vyžiadať overenie.", "match_interval": "Na základe pozorovanej frekvencie aktualizácií (medián={median}s) * 10.", + "match_interval_capped": "Obmedzené tak, aby sa {persistence} po sebe idúcich zhôd zmestilo do {pct}% najkratšieho programu ({shortest}s); samotná frekvencia aktualizácií (medián={median}s) by navrhla dlhší interval.", + "match_interval_floored": "Samotný najkratší program ({shortest}s) by túto hodnotu obmedzil na {cap}s, teda pod minimum {minimum}s, takže zostáva na minime: séria {persistence} po sebe idúcich zhôd trvá {budget}s, čiže viac než {pct}% tohto programu. Rozpoznávanie prebieha len vtedy, keď príde nové meranie (medián={median}s), takže minimum nestojí nič navyše.", "max_duration_ratio": "Na základe dĺžok označených cyklov (p95={p95}).", "min_active": "Na základe minimálneho činného výkonu ({min}W) zaznamenaného v poslednom cykle.", "min_duration_ratio": "Ponechané čo najnižšie, aby sa program rozpoznal skoro v cykle; prahy istoty a nejednoznačnosti bránia predčasnému potvrdeniu.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Nahrávanie zastavené", "review_saved": "Kontrola uložená", "settings_conflicts": "Pred uložením opravte zvýraznené konflikty nastavení.", + "settings_no_changes": "Žiadne zmeny na uloženie", "settings_reverted": "Nastavenia boli obnovené; integrácia sa znovu načítava", "settings_saved": "Nastavenia uložené; integrácia sa znovu načítava", "suggestions_applied": "Návrhy použité; integrácia sa znovu načítava", @@ -1949,7 +1985,11 @@ "export_selective_done": "Export stiahnutý", "import_selective_done": "Importovaných {profiles} profilov a {cycles} cyklov", "hist_csv_required": "Najprv načítajte súbor CSV alebo prilepte jeho obsah", - "file_read_failed": "Tento súbor sa nepodarilo prečítať" + "file_read_failed": "Tento súbor sa nepodarilo prečítať", + "unlabel_done": "Odobratý štítok pre {count} cyklov", + "unlabel_failed": "Odobratie štítka zlyhalo: {error}", + "odometer_saved": "Celkový počet cyklov uložený", + "odometer_save_failed": "Celkový počet sa nepodarilo uložiť: {error}" }, "trend": { "down": "Klesajúci trend", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Umývačka: tiché sekundy po očakávanom trvaní, než sa uvoľní čakanie na záverečné odčerpanie vody", "smart_termination_duration_ratio": "Zlomok očakávanej doby trvania priradeného programu, ktorý musí cyklus dosiahnuť, než ho Inteligentné ukončenie môže ukončiť skôr; znížte ho pri strojoch závislých od náplne alebo teploty", + "anti_crease_finalize_ratio": "Zlomok očakávanej doby trvania priradeného programu, ktorý musí cyklus dosiahnuť, než možno ukončiť chvost otáčania ochrany pred krčením; znížte ho pri sušičkách s dlhým chvostom otáčania, pri práčkach ponechajte predvolenú hodnotu", "anti_wrinkle_enabled": "Pohltiť impulzy bubna po hlavnej fáze namiesto ich čítania ako nové cykly", "anti_wrinkle_exit_power": "Výkon musí medzi impulzmi klesnúť pod túto hodnotu, aby ochrana pred krčením zostala aktívna", "anti_wrinkle_idle_timeout": "Povolený pokoj medzi dvoma impulzmi bubna, než sa ochrana pred krčením skončí", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Fáza 3: vzdialenosť polosaturácie DDTW; menšia = citlivejšia na tvar (predvolené 30)", "dtw_refine_top_n": "Fáza 3: kandidáti, ktorých DTW prehodnotí; zvýšte na 7-9, ak správny profil skončí na 4.-5. mieste (predvolené 5)", "duration_scale": "Fáza 4: logaritmický pomer, pri ktorom sa zhoda dĺžky trvania zníži na polovicu; menší = prísnejšia penalizácia (predvolené 0,175)", - "energy_scale": "Fáza 4: logaritmický pomer, pri ktorom sa zhoda energie zníži na polovicu; menší = prísnejšia penalizácia (predvolené 0,25)" + "energy_scale": "Fáza 4: logaritmický pomer, pri ktorom sa zhoda energie zníži na polovicu; menší = prísnejšia penalizácia (predvolené 0,25)", + "curve_preroll_seconds": "Sekundy meraní z neúspešných pokusov o spustenie prenesené na začiatok krivky cyklu; 0 = vypnuté" }, "col": { "profile_tip": "Názov zhodného programu. Bez štítku znamená, že na konci cyklu sa nezhodoval žiadny profil.", @@ -1998,10 +2040,11 @@ "date_tip": "Dátum a čas začatia cyklu.", "duration_tip": "Celkový čas behu cyklu od začiatku do konca.", "energy_tip": "Celková spotrebovaná energia (kWh). Vypočítané integráciou výkonu v čase.", - "cost_tip": "Náklady na energiu pre tento cyklus, zafixované pri dokončení podľa ceny platnej v tom čase (energia x cena za kWh). Nastavte cenu v Nastaveniach, aby ste ho vyplnili.", + "cost_tip": "Náklady na energiu za tento cyklus, zafixované pri dokončení. Keď sú Časovo vážené náklady zapnuté, krivka výkonu sa účtuje cenou platnou v každom okamihu; inak energia x jediná cena platná na konci. Nastavte cenu v Nastaveniach, aby sa hodnota vyplnila.", "confidence_tip": "Spoľahlivosť zhody profilu (0-100%). Ako presne krivka výkonu cyklu zodpovedala identifikovanému programu.", "health_tip": "Zdravie cyklu ML (vyššie = lepšie). Kliknutím na cyklus ho skontrolujete a zhodnotíte.", - "flags_tip": "Komunitné príznaky a indikátory kvality dát" + "flags_tip": "Komunitné príznaky a indikátory kvality dát", + "cost_dynamic_tip": "Časovo vážené: účtované cenou platnou počas cyklu (efektívne {price} {cur}/kWh)." }, "stat": { "avg": "priem.", @@ -2028,9 +2071,9 @@ "finished": "Cyklus dosiahol koncový stav a skončil." }, "store": { + "search_ph2": "Hľadať podľa značky alebo modelu…", "your_model_tip": "Toto je spotrebič, ktorý ste zadali v Nastaveniach", "your_model": "Vaše", - "search_brand_ph": "Hľadať podľa značky…", "programs_count": "Programy: {n}", "browse": "Prehliadať", "device": "Zariadenie", @@ -2075,7 +2118,8 @@ "golden": "Opätovné spracovanie: dopĺňanie referenčných", "suggestions": "Opätovné spracovanie: návrhy", "ml_training": "Opätovné spracovanie: tréning ML", - "health": "Opätovné spracovanie: kvalita cyklov" + "health": "Opätovné spracovanie: kvalita cyklov", + "costs": "Opätovné spracovanie: náklady na energiu" }, "pg_sweep": { "optimize": "Optimalizácia: {param}" diff --git a/custom_components/ha_washdata/translations/panel/sl.json b/custom_components/ha_washdata/translations/panel/sl.json index 800b84eb..0d7fc49b 100644 --- a/custom_components/ha_washdata/translations/panel/sl.json +++ b/custom_components/ha_washdata/translations/panel/sl.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Proti koncu ta program za približno {mins} min utihne, nato pa pred zaključkom {secs} s porablja okoli {watts} W. Zaznano pri {seen} od {measured} izmerjenih ciklov: naprava tega ne stori ob vsakem zagonu.", + "quiet_tail": "tih konec ~{mins} min", "artifact_tip": "{n} zaznanih nepravilnosti (npr. odprta vrata sredi cikla) – odprite, da jih vidite na grafu", "auto": "(samodejno zaznano)", "built_in_tag": "vgrajeno", @@ -77,11 +79,14 @@ "approved": "Odobreno", "awaiting": "Čaka na odobritev", "imported_tip": "Uvoženo iz skupnostne trgovine. Uporablja se samo za ujemanje, ne šteje se v statistiko.", + "unmatchable": "ni mogoče ujemati", "not_importable": "ni na voljo", "exists": "obstaja", - "backfilled_tip": "Zaznano v uvoženi zgodovini moči. Vpliva samo na ujemanje programov, ne šteje se v statistiko." + "backfilled_tip": "Zaznano v uvoženi zgodovini moči. Vpliva samo na ujemanje programov, ne šteje se v statistiko.", + "armed": "(velja za naslednji cikel)" }, "btn": { + "reset_zoom": "Ponastavi povečavo", "set_brand_model": "Nastavi znamko in model", "refresh_catalog": "Osveži katalog", "add_device": "+ Dodaj napravo", @@ -217,6 +222,8 @@ "trim": "Obreži", "trim_split": "Obreži / Razdeli", "undo": "Razveljavi", + "unlabel_selected": "Odstrani oznako izbranim ({n})", + "unlabel_selected_tip": "Odstrani oznako profila z izbranih ciklov. Ostanejo v vaši zgodovini in jih je mogoče pozneje znova označiti - le tega profila ne oblikujejo več.", "use": "Uporabi", "wipe_all": "Izbriši vse podatke", "delete_selected": "Izbriši izbrano ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Uporabi najboljše", "pg_apply_to_settings": "Shrani v nastavitve", "pg_apply_to_settings_tip": "Kopiraj tukaj urejene vrednosti v aktivne nastavitve te naprave", - "search": "Išči", "import": "Uvozi", "disconnect": "Prekini povezavo", "connect_github": "Poveži GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Preberi iz Home Assistant", "hist_scan": "Poišči cikle", "hist_import_n": "Uvozi cikle: {n}", - "hist_goto_cycles": "Pokaži cikle" + "hist_goto_cycles": "Pokaži cikle", + "save_total_cycles": "Shrani skupno število" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Mora biti nad Pragom izklopa napajanja ({pot} W)", "threshold": "Mora biti pod Pragom zaustavitve ({stop} W), da ima učinek" }, + "price_entity": { + "power_sensor": "To je Senzor moči naprave, ne cena. Entiteta cene ima prednost pred statično ceno spodaj, zato bi se cikli obračunali po vatih za kWh - počistite to polje.", + "energy_sensor": "To je Merilnik energije naprave, ne cena. Entiteta cene ima prednost pred statično ceno spodaj, zato bi se cikli obračunali po odčitku merilnika za kWh - počistite to polje.", + "device_class": "Razred tega senzorja je {dc}, ne cena za kWh. Entiteta cene ima prednost pred statično ceno spodaj - počistite to polje ali ga usmerite na senzor tarife.", + "unit": "Ta senzor meri v {unit}, kar ni cena za kWh. Entiteta cene ima prednost pred statično ceno spodaj - počistite to polje ali ga usmerite na senzor tarife." + }, "pump_stuck": { "duration": "Mora biti manjši od Časovne omejitve brez posodobitve ({to} s), da alarm zastoja sproži pred tem, ko nadzornik prekine cikel", "timeout": "Mora presegati Trajanje zataknjenosti črpalke ({ps} s), da alarm sproži pred prisilno zaustavitvijo cikla" @@ -349,7 +362,7 @@ "pg_settings_source": "Vir nastavitev", "phase_catalog": "Katalog faz", "playground": "Peskovnik", - "process_history": "Zgodovina procesov", + "process_history": "Obdelava zgodovine", "settings_history": "Zgodovina nastavitev", "status": "Stanje", "status_graph": "Graf stanja", @@ -360,12 +373,12 @@ "wipe_history": "Izbriši zgodovino", "recommendations": "Priporočila ({n})", "pg_detection_params": "Nastavitve zaznavanja", - "pg_timeline": "Časovnica dogodkov", "pg_outcome": "Izid simulacije", "pg_across_cycles": "Po vseh vaših ciklih", "community_store": "Skupnostna trgovina", "online_account": "Skupnostna trgovina in spletne funkcije", - "import_power_history": "Uvoz zgodovine moči" + "import_power_history": "Uvoz zgodovine moči", + "service_status": "Stanje servisa" }, "health": { "fair": "Sprejemljiva kakovost profila", @@ -373,6 +386,10 @@ "poor": "⚠ Slaba kakovost profila" }, "lbl": { + "envelope_position_tip": "Položaj na lastni posneti krivulji ujemajočega programa, {pct}%. Določen je s primerjavo oblike tega cikla s profilom in ne s štetjem časa, zato ostane pravilen, kadar je zagon daljši ali krajši od običajnega. Osveži se, ko naprava miruje, zato lahko med aktivno fazo zaostaja.", + "envelope_position": "krivulja {pct}%", + "aria_playground_chart3": "Interaktivni graf moči cikla: pomaknite kazalec za prikaz časa/moči, drsajte ali uporabite dva prsta za povečavo, povlecite za premikanje", + "device_unresolved": "Nedostopna naprava ({id})", "drag_to_resize": "Povlecite za spremembo velikosti", "actions": "Dejanja", "activity": "Aktivnost", @@ -509,7 +526,7 @@ "profile_index": "indeks profila →", "profile_name": "Ime profila", "program": "Program", - "program_tip": "Preglasujte profil, ki je primerjan s trenutnim ciklom. Samozaznavanje omogoča integraciji, da samodejno izbere najboljše ujemanje. Pripnite določen program, da prisilite ujemanje, ko je samodejno zaznavanje napačno ali veste, kaj se izvaja.", + "program_tip": "Preglasujte profil, ki je primerjan s trenutnim ciklom. Samozaznavanje omogoča integraciji, da samodejno izbere najboljše ujemanje. Pripnite določen program, da prisilite ujemanje, ko je samodejno zaznavanje napačno ali veste, kaj se izvaja. Izberite program pred zagonom naprave in uporabljen bo takoj, ko se začne naslednji cikel.", "progress": "Napredek", "raw_socket": "Surova vtičnica", "read_only": "Samo za branje", @@ -544,6 +561,7 @@ "stage4": "Stopnja 4 – ujemanje", "status": "Stanje", "tail_trim": "Obrezovanje konca (s)", + "tap_to_dismiss": "tapnite graf, da zaprete", "timer_auto_pause": "Samodejna pavza", "timer_min": "min", "timer_msg_placeholder": "Sporočilo (izbirno, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Preostali čas (model)", "pg_time_left_model_tip": "Modelska ocena preostalega časa (ocenjevalnik faz + mešanje z ML), ne statično odštevanje", "phase": "Faza", - "pg_mode_simulate": "Simuliraj", "pg_mode_history": "Preizkusi na zgodovini", - "pg_mode_sweep": "Preizkušanje", "pg_ev_detected": "Zaznano", "pg_ev_match": "Ujemanje potrjeno", "pg_ev_match_changed": "Ujemanje spremenjeno", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Cilj", "pg_steps": "Koraki", - "pg_sweep_2d": "Toplotna karta 2D", - "pg_sweep_param_y": "2. parameter", "pg_obj_match": "Natančnost ujemanja", "pg_obj_endtiming": "Natančnost konca", "pg_obj_falseend": "Stopnja lažnih koncev", "pg_obj_overrun": "Odstopanje trajanja", "pg_obj_ambiguity": "Stopnja dvoumnosti", - "pg_best_combo": "najboljša kombinacija", "pg_mode_optimize": "Optimiziraj", "pg_group_matching": "Ujemanje programov", "pg_zoom_hint": "dvojni klik ponastavi povečavo", @@ -706,7 +719,12 @@ "task_history_import_apply": "Uvažanje ciklov", "evidence_real_cycles": "Cikli, ki jih je izvedla ta naprava", "evidence_reference_cycles": "Preneseni iz skupnostne trgovine", - "evidence_backfill_cycles": "Najdeni v uvoženi zgodovini moči" + "evidence_backfill_cycles": "Najdeni v uvoženi zgodovini moči", + "zoom_hint_touch": "povečajte z dvema prstoma", + "zoom_hint_touch_zoomed": "povečajte z dvema prstoma · dvakrat tapnite za ponastavitev", + "cycles_since_service": "{since} / {total} ciklov", + "total_cycles_run": "ciklov skupaj", + "correct_total_cycles": "Popravi skupno število" }, "log": { "all_levels": "Vse ravni", @@ -756,7 +774,7 @@ "merge_cycles": "Spoji {n} ciklov", "new_group": "Nova skupina profilov", "new_phase": "Nova faza", - "process_history_msg": "Znova zaženite ujemanje, osvežite predloge, naučite ML (če je omogočen) in znova izračunajte zdravje ciklov za vse shranjene cikle. To lahko traja nekaj časa.", + "process_history_msg": "Znova zaženite ujemanje, osvežite predloge, znova naučite ML (če je omogočen), znova izračunajte stroške glede na zabeležene cene energije in znova izračunajte zdravje ciklov za vse shranjene cikle. To lahko traja nekaj časa.", "process_history_ok": "Obdelaj", "process_history_title": "Obdelaj zgodovino", "process_recording": "Obdelaj posnetek", @@ -789,6 +807,7 @@ "history_import": "Uvoz zgodovine moči" }, "msg": { + "pg_canvas_empty3": "Izberite cikel zgoraj in pritisnite Zaženi za simulacijo. Nato pomaknite kazalec za prikaz vrednosti, drsajte ali uporabite dva prsta za povečavo in povlecite za premikanje.", "tail_trim_hint": "Odstranite toliko sekund s konca", "store_sibling_hint": "Za vaš točni model ni nič deljenega? Tesno soroden model iste znamke je običajno dobro izhodišče.", "store_declare_appliance": "Povejte integraciji WashData, katero napravo imate, in ta zavihek bo prikazal konfiguracije, ki so jih zanjo delili drugi. Zgoraj lahko tudi vpišete znamko in pregledate katalog.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Presegl normalni pas moči za ~{n} s.", "auto_label_intro": "Dodelite profile neoznačenim ciklom, katerih ujemanje presega prag.", "automations_intro": "WashData sproži dogodke {start} / {end} in razkrije entitete, zato je obvestila in dejanja najbolje graditi kot navadne avtomatizacije Home Assistant. Spodaj so prikazane avtomatizacije, ki uporabljajo to napravo.", - "cleanup_intro": "Vsi označeni cikli naloženi. Označi odmike in briši za čiščenje profila.", + "cleanup_intro": "Vsi označeni cikli so prekriti drug čez drugega. Obkljukajte odstopajoče cikle in jim nato odstranite oznako (ostanejo v zgodovini, a tega profila ne oblikujejo več) ali jih izbrišite.", "clear_debug_hint": "Odstranite shranjene podatke za odpravljanje napak, da sprostite prostor.", "collecting_data": "Zbiranje podatkov. Še potrebnih ciklov pred začetkom natančnega prilagajanja: {need} ({current}/{min}).", "compare_overlay_profiles": "Prekriti profili (bledi)", @@ -902,7 +921,7 @@ "preferences_adv": "Nastavitve", "prefs_personal": "Veljajo samo za vaš račun Home Assistant.", "font_size_hint": "Povečajte ali pomanjšajte vse v tej plošči. Velja za vaš račun na tej napravi.", - "process_history_hint": "Znova zaženite ujemanje za vse shranjene cikle, osvežite predloge nastavljanja, znova naučite modele ML (če so omogočeni) in znova izračunajte zdravje ciklov. Zaženite po seriji pregledov.", + "process_history_hint": "Znova zaženite ujemanje za vse shranjene cikle, osvežite predloge nastavljanja, znova naučite modele ML (če so omogočeni), znova izračunajte stroške ciklov glede na zabeležene cene energije in znova izračunajte zdravje ciklov. Zaženite to po seriji pregledov.", "profile_deleted": "Profil izbrisan", "profile_poor_health_detail": "Cikli, dodeljeni temu profilu, imajo nedosledne oblike ali nizko stopnjo zaupanja. Razmislite o ponovni izdelavi ovojnice ali pregledu označenih ciklov.", "profiles_intro": "Kliknite na profil za statistike, faze in čiščenje. Združite skoraj enake programe (enaka oblika/trajanje, različna temperatura ali ožemanje), da ujemanje zanesljivo izbira med njimi.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "Profil '{name}' ima nizko oceno ujemanja - njegovi nedavni cikli se močno razlikujejo ali se slabo ujemajo. Preglejte njegove cikle ali ponovno posnemite profil, da prepoznavanje in ocene časa ostanejo natančni.", "advisory_shape_drift": "Profil '{name}' je znatno odstopil od prvotne oblike porabe moči. Naprava je morda sčasoma spremenila vedenje (npr. vodni kamen, obraba). Razmislite o ponovnem snemanju tega profila z nedavnimi cikli.", "advisory_shape_drift_corr": "Profil '{name}' je znatno odstopil od prvotne oblike porabe moči (korelacija {corr}). Naprava je morda sčasoma spremenila vedenje (npr. vodni kamen, obraba). Razmislite o ponovnem snemanju tega profila z nedavnimi cikli.", + "advisory_unmatchable": "Profil '{name}' se ne more nikoli ujemati: za njim ni nobenega cikla s podatki o moči, zato WashData nima s čim primerjati cikla, ki poteka. Označite kateri cikel s tem programom (ali enega posnemite), da se profil vrne v ujemanje.", + "advisory_unmatchable_no_duration": "Profil '{name}' se ne more nikoli ujemati: ima cikel s podatki o moči, vendar brez uporabnega trajanja, zato WashData nima referenčne dolžine, po kateri bi se ujemanje merilo. Ta program posnemite znova ali nastavite njegovo pričakovano trajanje, da se vrne v ujemanje.", "pg_no_alerts": "V tem zagonu ni bilo zaznanih težav.", "pg_override_active": "Uporabljajo se vaše urejene nastavitve namesto trenutnih", - "pg_history_intro": "Ponovno predvajajte svoje nedavne cikle skozi pravi detektor in ujemalnik. Najprej uredite nastavitev zaznavanja v načinu Simuliraj, da tu vidite primerjavo pred/po.", "pg_history_empty": "Pritisnite Zaženi za ponovno predvajanje ciklov.", "pg_history_summary": "{detected}/{total} zaznanih · {correct} pravilno ujemanih", "pg_sweep_no_metric": "Premalo podatkov za oceno tega cilja.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Pregledovanje ni uspelo.", "hist_scan_expired": "Ta pregled ni več na voljo. Zaženite ga znova.", "hist_import_failed": "Uvoz ni uspel.", - "imported_history_readonly": "Zaznano v uvoženi zgodovini moči. Vpliva na ujemanje programov, ne šteje pa se v vašo statistiko in ga ni mogoče obrezati ali razdeliti. Označite ga, da mu določite program." + "imported_history_readonly": "Zaznano v uvoženi zgodovini moči. Vpliva na ujemanje programov, ne šteje pa se v vašo statistiko in ga ni mogoče obrezati ali razdeliti. Označite ga, da mu določite program.", + "odometer_intro": "To skupno število samo narašča. Brisanje zapisa o ciklu ga ne spremeni in šteje naprej tudi prek omejitve shranjene zgodovine, zato opomniki za servis ostanejo pravilni.", + "odometer_correct_hint": "Nastavite to, če je WashData zabeležil cikel, ki se nikoli ni zgodil, ali če je naprava opravila cikle že pred namestitvijo WashData.", + "no_reminders_set": "Opomniki za servis še niso nastavljeni.", + "toast_program_armed": "Program nastavljen za naslednji cikel: {program}" }, "phase_desc": { "anti_crease": "Občasni kratki premiki po zaključku za preprečevanje gub na oblačilih.", @@ -1161,6 +1185,10 @@ "label": "Sprostitev po tihi fazi sušenja", "doc": "Ko cikel preseže svoje pričakovano trajanje, kako dolgo mora pomivalni stroj ostati tih (pod Pragom zaustavitve), preden WashData preneha čakati na končno izčrpavanje vode in zaključi cikel. Povečajte, če ima vaša naprava dolgo tiho fazo sušenja pred poznim končnim izčrpavanjem, ki ga zaznavanje spregleda - širše okno omogoča, da naučeno trajanje sledi sezonskemu premiku (hladnejša dovodna voda = daljši cikli), namesto da ostane zaklenjeno na starem povprečju. Čakanje lahko le skrajša glede na notranjo 30-minutno omejitev čakanja na končno izčrpavanje, nikoli ga ne podaljša." }, + "anti_crease_finalize_ratio": { + "doc": "Kako daleč v pričakovanem trajanju ujemajočega programa mora biti cikel, preden sme WashData sklepati, da je tiho vrtenje bobna, ki ga vidi, že zaključni rep zaščite pred gubami, in cikel tam končati. Pričakovano trajanje je povprečje programa, zato pri sušilnem stroju, katerega čas senzorskega sušenja se ravna po količini perila, večina zagonov nikoli ne doseže 98% tega povprečja in rep obstane vse do nadomestnega časovnega zamika - pogosto 15 do 40 minut po tem, ko se je stroj dejansko ustavil. Pri takem sušilnem stroju to vrednost znižajte (npr. 0,75). Pralne stroje pustite na privzeti vrednosti: pralni stroj preživi večji del cikla pod nastavitvijo Maks. moč zaščite pred gubami, zato je ta delež edino, kar loči tiho fazo sredi pranja od repa, njegovo znižanje pa lahko pranje prepolovi. Pustite prazno za privzeto vrednost (0,98). To je druga nastavitev kot Razmerje pametnega zaključka, ki ureja ločeno preverjanje predčasnega zaključka.", + "label": "Razmerje zaključka zaščite pred gubami" + }, "anti_wrinkle_enabled": { "doc": "Prepoznajte kratke vrtilne impulze nizke moči, ki jih oddaja sušilni stroj po glavni fazi segrevanja, in jih ohranite povezane s končanim ciklom, namesto da jih berete kot nove cikle.", "label": "Omogoči zaznavanje zaščite pred gubami" @@ -1193,6 +1221,10 @@ "doc": "Cikli, krajši od tega, se zavržejo kot cikli duhov (testne vožnje, odpiranje vrat za dodajanje nogavice).", "label": "Min. trajanje cikla" }, + "curve_preroll_seconds": { + "doc": "Koliko sekund odčitkov iz prekinjenih poskusov zagona sme WashData prenesti na začetek krivulje cikla. Krivulja cikla se običajno začne pri poskusu zagona, ki končno uspe, zato pri stroju, ki pred umiritvijo večkrat poskusi - izbira programa, zaklep vrat, prvo polnjenje z vodo - v vsaki krivulji manjkajo prve minute resnične dejavnosti. To vrednost nastavite nekoliko nad najdaljši tak zalet, ki ga ima vaš stroj (300 s je za večino več kot dovolj). 0 funkcijo izklopi. Upoštevajte, da vklop premakne zabeleženi začetek nazaj, zato cikli, zabeleženi pred spremembo in po njej, za isti program nosijo različna trajanja; naučena povprečja bodo odstopala, dokler starejši cikli ne izpadejo.", + "label": "Uvodne sekunde krivulje" + }, "delay_confirm_seconds": { "doc": "Napajanje mora ostati v pasu pripravljenosti toliko časa, preden se naprava obravnava kot naprava, ki čaka na zagon, namesto da deluje.", "label": "Sekunde potrditve" @@ -1230,7 +1262,7 @@ "label": "Število ponovitev konca" }, "energy_price_entity": { - "doc": "Senzor s trenutno ceno električne energije na kWh (npr. dinamična tarifa). Ima prednost pred spodnjo statično ceno. Vsak cikel zamrzne veljavno ceno, ko se konča.", + "doc": "Senzor s trenutno ceno elektrike na kWh (npr. dinamična tarifa). Biti mora cena, ne lastna entiteta moči ali energije vtičnice: ima prednost pred statično ceno spodaj, zato bi tukaj vpisan števec kWh vsak cikel obračunal po odčitku merilnika namesto po vaši tarifi. Ko so Časovno tehtani stroški vklopljeni, se vsak cikel obračuna po ceni, ki je veljala v vsakem trenutku njegovega teka; sicer se nanj zamrzne cena, ki je veljala ob zaključku.", "label": "Entiteta cene energije" }, "energy_price_static": { @@ -1258,7 +1290,7 @@ "label": "Obrni zunanji sprožilec (sproži ob IZKLOP)" }, "learning_confidence": { - "doc": "Če je rezultat ujemanja med tem in zaupanjem samodejnega označevanja, vas obvestilo s povratnimi informacijami prosi, da preverite prepoznani program. Pod tem rezultatom je tekma preveč negotova, da bi prišla na površje. Mora biti pod zaupanjem samodejnega označevanja.", + "doc": "Če je rezultat ujemanja med to vrednostjo in Zaupanjem samodejnega označevanja, vas obvestilo s povratnimi informacijami prosi, da preverite prepoznani program. Pod tem rezultatom je ujemanje preveč negotovo, da bi ga prikazali. Ostati mora pod Zaupanjem samodejnega označevanja.", "label": "Zaupanje učenja" }, "linked_device": { @@ -1326,7 +1358,7 @@ "label": "Sproži HA dogodke za obvestila" }, "notify_icon": { - "doc": "Izbirna ikona mdi za obvestilo (npr. mdi:washing-machine). Pustite prazno za privzeto platformo.", + "doc": "Majhna ikona, prikazana v obvestilu (npr. mdi:washing-machine). Android jo izriše v vrstici stanja; iOS jo prikaže namesto ikone aplikacije in zahteva mobilno aplikacijo Home Assistant (companion app) 2026.8 ali novejšo. Pustite prazno za privzeto ikono platforme.", "label": "Ikona obvestila" }, "notify_live_chronometer": { @@ -1426,7 +1458,7 @@ "label": "Prag ujemanja" }, "profile_unmatch_threshold": { - "doc": "Če tekma v živo sredi cikla pade pod ta rezultat, se začasna identifikacija počisti. Naj bo malo pod pragom ujemanja, tako da kratek padec podobnosti ne obrne zaslona nazaj na neizravnanega.", + "doc": "Če ujemanje v živo sredi cikla pade pod ta rezultat, se začasna identifikacija počisti. Naj bo malo pod Pragom ujemanja, tako da kratek padec podobnosti prikaza ne vrne nazaj na stanje brez ujemanja.", "label": "Prag neujemanja" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Nekaj sekund lahko črpalka deluje neprekinjeno, preden je označena kot morebitna zastoj (sproži dogodek zastoja črpalke).", "label": "Trajanje zataknjenosti črpalke" }, - "running_dead_zone": { - "doc": "Ko se cikel začne, se padci moči v tem oknu ne upoštevajo. Pralni stroj se polni s hladno vodo (pred segrevanjem pade blizu 0 W) – brez te zaščite je faza polnjenja videti kot konec cikla. To NE preskoči podatkov: sled polne moči se zabeleži od T=0. Mehanizem predlogov meri dejanski vzorec zagona vašega računalnika in ga samodejno prilagodi.", - "label": "Mrtva cona delovanja" - }, "sampling_interval": { "doc": "Pričakovan čas med odčitki senzorja – uporablja se za velikost okna za glajenje in pravilen začetek odboja. Vsaka posodobitev senzorja je zajeta ne glede na to vrednost; kalibrira le spodnje izračune. Mehanizem predlogov meri dejansko kadenco vašega senzorja iz preteklih ciklov in jo samodejno nastavi.", "label": "Interval vzorčenja" @@ -1538,7 +1566,7 @@ }, "keep_min_score": { "label": "Min. ocena ujemanja", - "doc": "Minimalna ocena podobnosti, ki jo mora imeti kandidat, da ostane v tekmi za ujemanje. Privzeta vrednost 0,1 je namerno dovoljna - sprejme celo šibke kandidate in se zanaša na kasnejše stopnje za iskanje najboljšega ujemanja. Zvišajte za zgodnejše izločanje malo verjetnih profilov; znižajte za razširitev začetnega nabora kandidatov." + "doc": "Minimalna ocena podobnosti, ki jo mora imeti kandidat, da ostane v izboru za ujemanje. Privzeta vrednost 0,1 je namerno dopustna - sprejme tudi šibke kandidate in se zanaša na kasnejše stopnje, da najdejo najboljše ujemanje. Zvišajte jo za zgodnejše izločanje malo verjetnih profilov; znižajte jo za razširitev začetnega nabora kandidatov." }, "dtw_blend": { "label": "Mešanje deformacije", @@ -1568,13 +1596,17 @@ "label": "Interval profila porabe", "doc": "Velikost segmenta za atribut senzorja power_profile za posamezni profil (ploska matrika povprečnih vatov na režo, ki jo porabijo zunanji planerji, kot sta EMHASS in tibber_prices). Manjši segmenti ohranijo kratke skoke moči ostre; večji segmenti zgladijo obliko. Privzeto 15 min. Samo za branje; ne vpliva na zaznavanje." }, + "notify_live_silent": { + "label": "Tihe posodobitve v živo", + "doc": "Samo iOS. Napredek v živo osvežujte tiho: vsaka posodobitev prispe brez zvoka in vibriranja, z nižjo prednostjo, ki varčuje z baterijo in jo lahko iOS za kratek čas združi. Prva posodobitev v ciklu (tista, ki zažene Live Activity) in obvestila o začetku ter koncu se še naprej oglašajo kot običajno. To izklopite, če želite opozorilo ob vsaki posodobitvi." + }, "notify_live_sticky": { "label": "Ohrani živo obvestilo ob dotiku", "doc": "Samo Android. Živo obvestilo o napredku nastavite kot trajno (pritrjeno), da ga dotik ne zavrže. Izklopljeno ohranja privzeto vedenje, ko dotik zavrže obvestilo." }, "notify_live_click_action": { - "label": "Cilj dotika na živo obvestilo", - "doc": "Samo Android. Kam dotik na živo obvestilo o napredku odpre (npr. /lovelace/laundry ali celoten URL) namesto vstopne strani aplikacije. Pustite prazno za privzeto." + "label": "Cilj dotika na obvestilo", + "doc": "Kam vodi dotik obvestila te naprave, v sistemih Android in iOS. Prazno polje odpre ploščo WashData na tej napravi. Vnesite pot nadzorne plošče (npr. /lovelace/laundry) ali celoten URL, če naj vodi drugam, ali „none“, če dotik ne sme odpreti ničesar." }, "door_opens_at_end": { "label": "Vrata se ob koncu samodejno odprejo", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Cikli, ki oblikujejo program", "doc": "Kateri cikli se uporabijo za izgradnjo krivulje moči vsakega programa in za ujemanje zaključenega cikla z njo. Če neko vrsto odkljukate, ta ne vpliva več na vaše programe, vendar se nič ne izbriše - cikli ostanejo na seznamu Cikli in jih je še vedno mogoče označiti ali odstraniti. Uporabno, če uvoženim podatkom ne zaupate. Na statistiko to ne vpliva: vedno šteje samo cikle, ki jih je ta naprava res izvedla. Če odkljukate vse, se to prezre, saj se program brez ciklov nikoli ne bi mogel ujemati." + }, + "energy_price_dynamic": { + "label": "Časovno tehtani stroški", + "doc": "Vsak cikel obračuna po ceni, ki je veljala v vsakem trenutku njegovega teka, namesto po eni sami ceni ob zaključku. Velja samo za entiteto cene (statična cena se ne more spreminjati). Ne potrebuje dodatnih nastavitev: cena se med ciklom spremlja v živo, Obdelava zgodovine v Diagnostiki pa starejšim ciklom znova izračuna stroške iz recorderja. Izklopljeno = klasično vedenje, ena cena, zamrznjena ob koncu." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Deljenje…", "deleting": "Brisanje…", "imported": "Uvoženo", - "preparing": "Pripravljanje…" + "preparing": "Pripravljanje…", + "unlabelling": "Odstranjevanje oznake…" }, "suggestion": { "both_agree": "WashData priporoča", @@ -1745,10 +1782,6 @@ "higher": "Potrdi le visoko zanesljiva ujemanja; več ciklov ostane neoznačenih", "lower": "Ujema več ciklov; nekateri so lahko rahlo napačni" }, - "running_dead_zone": { - "higher": "Prezre kratke skoke moči med mirovanjem – manj občutljiv na šum", - "lower": "Odziva se na krajše impulze moči – zazna hitro prehodno aktivnost" - }, "start_threshold_w": { "higher": "Preprečuje lažne zagone iz kratkih skokov moči", "lower": "Zazna programe z nizko močjo; lahko se odzove na šum" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} čistih ciklov ({pct}%) je imelo notranji premor, daljši od 60s, ki se je nadaljeval; zahtevaj {val} potrditev konca.", "learning_confidence": "Zaupanje p05 za {cycles} uporabniško označenih ciklov ({p05}); pod to vrednostjo zahtevaj preverjanje.", "match_interval": "Na podlagi opažene frekvence posodobitev (mediana={median}s) * 10.", + "match_interval_capped": "Omejeno tako, da {persistence} zaporednih ujemanj sodi v {pct}% najkrajšega programa ({shortest}s); sama frekvenca posodobitev (mediana={median}s) bi predlagala daljši interval.", + "match_interval_floored": "Že sam najkrajši program ({shortest}s) bi to vrednost omejil na {cap}s, pod minimum {minimum}s, zato ostane na minimumu: niz {persistence} zaporednih ujemanj traja {budget}s, kar je več kot {pct}% tega programa. Ujemanje se izvede le, ko prispe nov odčitek (mediana={median}s), zato minimum ne povzroči dodatne obremenitve.", "max_duration_ratio": "Na podlagi trajanj označenih ciklov (p95={p95}).", "min_active": "Na podlagi najmanjše delovne moči ({min}W), opažene v zadnjem ciklu.", "min_duration_ratio": "Ohranjeno čim nižje, da se program prepozna zgodaj v ciklu; pragova zaupanja in dvoumnosti preprečujeta prezgodnje potrditve.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Snemanje ustavljeno", "review_saved": "Pregled shranjen", "settings_conflicts": "Pred shranjevanjem popravite označene konflikte nastavitev.", + "settings_no_changes": "Ni sprememb za shranjevanje", "settings_reverted": "Nastavitve obnovljene; integracija se znova nalaga", "settings_saved": "Nastavitve shranjene; integracija se znova nalaga", "suggestions_applied": "Predlogi uporabljeni; integracija se znova nalaga", @@ -1949,7 +1985,11 @@ "export_selective_done": "Izvoz prenesen", "import_selective_done": "Uvoženih {profiles} profilov in {cycles} ciklov", "hist_csv_required": "Najprej naložite datoteko CSV ali prilepite njeno vsebino", - "file_read_failed": "Te datoteke ni bilo mogoče prebrati" + "file_read_failed": "Te datoteke ni bilo mogoče prebrati", + "unlabel_done": "Odstranjena oznaka za {count} ciklov", + "unlabel_failed": "Odstranjevanje oznake ni uspelo: {error}", + "odometer_saved": "Skupno število ciklov shranjeno", + "odometer_save_failed": "Skupnega števila ni bilo mogoče shraniti: {error}" }, "trend": { "down": "Padajoči trend", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Pomivalni stroj: sekunde tišine po pričakovanem trajanju, preden se sprosti čakanje na končno izčrpavanje vode", "smart_termination_duration_ratio": "Delež pričakovanega trajanja ujemajočega programa, ki ga mora cikel doseči, preden ga Pametni zaključek lahko predčasno konča; znižajte ga pri strojih, odvisnih od količine perila ali temperature", + "anti_crease_finalize_ratio": "Delež pričakovanega trajanja ujemajočega programa, ki ga mora cikel doseči, preden se sme zaključni rep vrtenja zaščite pred gubami zaključiti; znižajte ga pri sušilnih strojih z dolgim repom vrtenja, pralne stroje pustite na privzeti vrednosti", "anti_wrinkle_enabled": "Vsrkaj vrtilne impulze po glavni fazi, namesto da jih obravnavaš kot nove cikle", "anti_wrinkle_exit_power": "Moč mora med impulzi pasti pod to vrednost, da zaščita pred gubami ostane aktivna", "anti_wrinkle_idle_timeout": "Dovoljeni mir med dvema vrtilnima impulzoma, preden se zaščita pred gubami konča", @@ -1984,13 +2025,14 @@ "energy_weight": "Stopnja 4: kako močno ujemanje energije vpliva na končno oceno (privzeto 0,22)", "profile_match_min_duration_ratio": "Stopnja 1: minimalna dolžina cikla kot del trajanja profila; privzeto 0,1 (10%)", "profile_match_max_duration_ratio": "Stopnja 1: maksimalna dolžina cikla kot del trajanja profila; privzeto 1,5 (150%)", - "keep_min_score": "Stopnja 2: minimalna ocena za ostanok v tekmi; privzeto 0,1 sprejme šibka ujemanja, kasnejše stopnje izberejo najboljše", + "keep_min_score": "Stopnja 2: najnižja ocena za ostanek v izboru; privzeto 0,1 sprejme šibka ujemanja, kasnejše stopnje izberejo najboljše", "dtw_blend": "Stopnja 3: 0 = samo osnovna ocena, 1 = samo DTW ocena, 0,5 = enakomerno mešanje (privzeto)", "dtw_ensemble_w": "Stopnja 3 (kombinirani način): utež skaliranega L1 v primerjavi z izvedenim DTW; privzeto 0,7 daje prednost ravni", "dtw_ddtw_scale": "Stopnja 3: razdalja pol-nasičenja DDTW; manjša = bolj občutljiva na obliko (privzeto 30)", "dtw_refine_top_n": "Stopnja 3: kandidati, ki jih DTW znova oceni; zvišajte na 7-9, če pravi profil zasede 4.-5. mesto (privzeto 5)", "duration_scale": "Stopnja 4: logaritmično razmerje, pri katerem se ujemanje trajanja prepolovi; manjše = strožja kazen (privzeto 0,175)", - "energy_scale": "Stopnja 4: logaritmično razmerje, pri katerem se ujemanje energije prepolovi; manjše = strožja kazen (privzeto 0,25)" + "energy_scale": "Stopnja 4: logaritmično razmerje, pri katerem se ujemanje energije prepolovi; manjše = strožja kazen (privzeto 0,25)", + "curve_preroll_seconds": "Sekunde odčitkov iz prekinjenih poskusov zagona, prenesene na začetek krivulje cikla; 0 = izklopljeno" }, "col": { "profile_tip": "Ime ujemajočega programa. Brez oznake pomeni, da se ob koncu cikla ni ujemal noben profil.", @@ -1998,10 +2040,11 @@ "date_tip": "Datum in čas začetka cikla.", "duration_tip": "Skupni čas trajanja cikla od začetka do konca.", "energy_tip": "Skupna porabljena energija (kWh). Izračunano z integracijo moči skozi čas.", - "cost_tip": "Strošek energije za ta cikel, zamrznjen ob zaključku po takrat veljavni ceni (energija x cena na kWh). Nastavite ceno v Nastavitvah, da ga izpolnite.", + "cost_tip": "Strošek energije za ta cikel, zamrznjen ob zaključku. Ko so Časovno tehtani stroški vklopljeni, se krivulja moči obračuna po ceni, ki velja v vsakem trenutku; sicer energija x ena sama cena, ki velja ob koncu. Nastavite ceno v Nastavitvah, da se vrednost izpolni.", "confidence_tip": "Zaupanje ujemanja profila (0-100%). Kako natančno se je krivulja moči cikla ujemala z določenim programom.", "health_tip": "Zdravje cikla ML (višje = boljše). Kliknite cikel, da ga pregledate in ocenite.", - "flags_tip": "Oznake skupnosti in kazalniki kakovosti podatkov" + "flags_tip": "Oznake skupnosti in kazalniki kakovosti podatkov", + "cost_dynamic_tip": "Časovno tehtano: obračunano po ceni, ki je veljala med ciklom (efektivno {price} {cur}/kWh)." }, "stat": { "avg": "povpr.", @@ -2028,9 +2071,9 @@ "finished": "Cikel je dosegel končno stanje in se je končal." }, "store": { + "search_ph2": "Iskanje po znamki ali modelu…", "your_model_tip": "To je naprava, ki ste jo navedli v Nastavitvah", "your_model": "Vaša", - "search_brand_ph": "Iskanje po znamki…", "programs_count": "Programi: {n}", "browse": "Brskaj", "device": "Naprava", @@ -2075,7 +2118,8 @@ "golden": "Ponovna obdelava: dopolnjevanje referenčnih", "suggestions": "Ponovna obdelava: predlogi", "ml_training": "Ponovna obdelava: usposabljanje ML", - "health": "Ponovna obdelava: kakovost ciklov" + "health": "Ponovna obdelava: kakovost ciklov", + "costs": "Ponovna obdelava: stroški energije" }, "pg_sweep": { "optimize": "Optimizacija: {param}" diff --git a/custom_components/ha_washdata/translations/panel/sq.json b/custom_components/ha_washdata/translations/panel/sq.json index 5c864573..fb3571a6 100644 --- a/custom_components/ha_washdata/translations/panel/sq.json +++ b/custom_components/ha_washdata/translations/panel/sq.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Afër fundit ky program bie në qetësi për rreth {mins} min, pastaj tërheq rreth {watts} W për {secs} s përpara se të përfundojë. Vërejtur në {seen}/{measured} cikle të matura: pajisja nuk e bën këtë në çdo nisje.", + "quiet_tail": "~{mins} min qetësi në fund", "artifact_tip": "U zbuluan {n} anomali (p.sh. dera e hapur në mes të ciklit) – hapeni për t'i parë ato në grafik", "auto": "(zbuluar automatikisht)", "built_in_tag": "i integruar", @@ -55,7 +57,7 @@ "energy_low": "Energji më e ulët se zakonisht", "energy_spike": "Energji më e lartë se zakonisht", "fair_fit": "cilësi e pranueshme", - "fair_fit_tip": "Konsistencë e moderuar e ndeshjes – disa cikle të caktuara për këtë profil kanë rezultate më të ulëta besimi. Etiketoni më shumë cikle ose riregjistroni profilin për të përmirësuar saktësinë.", + "fair_fit_tip": "Konsistencë e moderuar e përputhjes – disa cikle të caktuara për këtë profil kanë rezultate më të ulëta besueshmërie. Etiketoni më shumë cikle ose riregjistroni profilin për të përmirësuar saktësinë.", "feedback_requested": "Kërkohet koment", "golden_cycle": "Cikli referues i regjistruar", "improving": "↗ në rritje", @@ -63,7 +65,7 @@ "needs_review": "Ka nevojë për rishikim", "overrun": "Zgjati më shumë se zakonisht", "poor_fit": "cilësi e keqe", - "poor_fit_tip": "Histori jokonsistente e ndeshjeve – merrni parasysh rindërtimin e këtij profili", + "poor_fit_tip": "Histori jokonsistente e përputhjeve – merrni parasysh rindërtimin e këtij profili", "restart_gap_tip": "{n} boshllëk rinisje të HA: gjurma e energjisë ka një vrimë", "reviewed": "Shqyrtuar", "steady": "→ i qëndrueshëm", @@ -77,11 +79,14 @@ "approved": "Miratuar", "awaiting": "Në pritje të miratimit", "imported_tip": "Importuar nga dyqani i komunitetit. Përdoret vetëm për përputhje, nuk llogaritet në statistika.", + "unmatchable": "s'mund të përputhet", "not_importable": "s'aplikohet këtu", "exists": "ekziston", - "backfilled_tip": "Zbuluar në historikun e importuar të fuqisë. Ndikon vetëm në përputhjen e programeve, nuk llogaritet në statistika." + "backfilled_tip": "Zbuluar në historikun e importuar të fuqisë. Ndikon vetëm në përputhjen e programeve, nuk llogaritet në statistika.", + "armed": "(zbatohet për ciklin tjetër)" }, "btn": { + "reset_zoom": "Rivendos zmadhimin", "set_brand_model": "Cakto markën dhe modelin", "refresh_catalog": "Rifresko katalogun", "add_device": "+ Shto pajisje", @@ -215,6 +220,8 @@ "trim": "Prerje", "trim_split": "Prerje / Ndarje", "undo": "Zhbëj", + "unlabel_selected": "Hiq etiketën nga të zgjedhurat ({n})", + "unlabel_selected_tip": "Heq etiketën e profilit nga ciklet e zgjedhura. Ato mbeten në historikun tuaj dhe mund të etiketohen sërish më vonë - thjesht nuk e formësojnë më këtë profil.", "use": "Përdor", "wipe_all": "Fshi të gjitha të dhënat", "delete_selected": "Fshi të zgjedhurat ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Zbato më të mirën", "pg_apply_to_settings": "Ruaj te cilësimet", "pg_apply_to_settings_tip": "Kopjo vlerat që redaktove këtu te cilësimet aktive të kësaj pajisjeje", - "search": "Kërko", "import": "Importo", "disconnect": "Shkëput", "connect_github": "Lidh GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Lexo nga Home Assistant", "hist_scan": "Kërko cikle", "hist_import_n": "Importo {n} cikle", - "hist_goto_cycles": "Shfaq ciklet" + "hist_goto_cycles": "Shfaq ciklet", + "save_total_cycles": "Ruaj totalin" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Duhet të jetë mbi Pragun e Fikjes së Energjisë ({pot} W)", "threshold": "Duhet të jetë nën Pragun e Ndalimit ({stop} W) për të hyrë në fuqi" }, + "price_entity": { + "power_sensor": "Ky është Sensori i fuqisë i pajisjes, jo një çmim. Një entitet çmimi ka përparësi ndaj çmimit statik më poshtë, prandaj ciklet do të faturoheshin sikur vlera në vat të ishte çmimi për kWh - pastroni këtë fushë.", + "energy_sensor": "Ky është matësi i energjisë i pajisjes, jo një çmim. Një entitet çmimi ka përparësi ndaj çmimit statik më poshtë, prandaj ciklet do të faturoheshin sikur leximi i matësit të ishte çmimi për kWh - pastroni këtë fushë.", + "device_class": "Ky sensor mat {dc}, jo një çmim për kWh. Një entitet çmimi ka përparësi ndaj çmimit statik më poshtë - pastroni këtë fushë ose zgjidhni një sensor tarife.", + "unit": "Ky sensor lexon në {unit}, e cila nuk është çmim për kWh. Një entitet çmimi ka përparësi ndaj çmimit statik më poshtë - pastroni këtë fushë ose zgjidhni një sensor tarife." + }, "pump_stuck": { "duration": "Duhet të jetë më i vogël se Koha e Skadimit pa Përditësim ({to} s) kështu që alarmi i bllokimit të ndizet para se rojtari të vrasë ciklin", "timeout": "Duhet të tejkalojë Kohëzgjatjen e Pompës së Bllokuar ({ps} s) kështu që alarmi të ndizet para se cikli të ndalet me forcë" @@ -349,7 +362,7 @@ "pg_settings_source": "Burimi i cilësimeve", "phase_catalog": "Katalog fazash", "playground": "Zona e provave", - "process_history": "Historiku i procesit", + "process_history": "Proceso historinë", "settings_history": "Historiku i cilësimeve", "status": "Statusi", "status_graph": "Grafiku i statusit", @@ -360,12 +373,12 @@ "wipe_history": "Fshi historikun", "recommendations": "Rekomandime ({n})", "pg_detection_params": "Cilësimet e zbulimit", - "pg_timeline": "Kronologjia e ngjarjeve", "pg_outcome": "Rezultati i simulimit", "pg_across_cycles": "Në të gjithë ciklet tuaja", "community_store": "Dyqani i Komunitetit", "online_account": "Dyqani i Komunitetit dhe veçoritë online", - "import_power_history": "Importo historikun e fuqisë" + "import_power_history": "Importo historikun e fuqisë", + "service_status": "Statusi i mirëmbajtjes" }, "health": { "fair": "Cilësi e pranueshme e profilit", @@ -373,6 +386,10 @@ "poor": "⚠ Cilësi e keqe e profilit" }, "lbl": { + "envelope_position_tip": "Pozicioni në kurbën e regjistruar të vetë programit të përputhur, {pct}%. Matet duke përputhur këtë cikël me profilin në vend që të numërohet koha, prandaj mbetet i saktë kur një nisje është më e gjatë ose më e shkurtër se zakonisht. Freskohet ndërsa pajisja është e qetë, prandaj gjatë një faze aktive mund të mbetet prapa.", + "envelope_position": "kurba {pct}%", + "aria_playground_chart3": "Grafik interaktiv i fuqisë së ciklit: kaloni kursorin për të lexuar kohën/fuqinë, rrëshqitni ose zmadhoni me dy gishta, tërhiqni për të lëvizur", + "device_unresolved": "Pajisje e padisponueshme ({id})", "drag_to_resize": "Tërhiqni për të ndryshuar madhësinë", "actions": "Veprime", "activity": "Aktivitet", @@ -508,7 +525,7 @@ "profile_index": "indeksi i profilit →", "profile_name": "Emri i profilit", "program": "Program", - "program_tip": "Anuloni profilin që përputhet me ciklin aktual. Zbulimi automatik lejon integrimin të zgjedhë automatikisht përputhjen më të mirë. Fiksoni një program specifik për të detyruar përputhjen kur zbulimi automatik është i gabuar ose e dini çfarë po funksionon.", + "program_tip": "Anuloni profilin që përputhet me ciklin aktual. Zbulimi automatik lejon integrimin të zgjedhë automatikisht përputhjen më të mirë. Fiksoni një program specifik për të detyruar përputhjen kur zbulimi automatik është i gabuar ose e dini çfarë po funksionon. Zgjidhni një program para se të nisni pajisjen dhe do të zbatohet sapo të fillojë cikli tjetër.", "progress": "Progresi", "raw_socket": "Fole e papërpunuar", "read_only": "Vetëm lexim", @@ -543,6 +560,7 @@ "stage4": "Etapa 4 – përputhja", "status": "Statusi", "tail_trim": "Prerje në fund (s)", + "tap_to_dismiss": "trokit grafikun për ta mbyllur", "timer_auto_pause": "Auto-pauzë", "timer_min": "min", "timer_msg_placeholder": "Mesazh (opsionale, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Koha e mbetur (modeli)", "pg_time_left_model_tip": "Koha e mbetur e vlerësuar nga modeli (vlerësuesi i fazës + përzierje ML), jo një numërim mbrapsht statik", "phase": "Faza", - "pg_mode_simulate": "Simulo", "pg_mode_history": "Testo në histori", - "pg_mode_sweep": "Skanim", "pg_ev_detected": "U zbulua", "pg_ev_match": "Përputhja u përcaktua", "pg_ev_match_changed": "Përputhja ndryshoi", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Anuloni vlerën minimale të gjendjes statike të zbuluar automatikisht (lini bosh për automatik).", "pg_stress_toggle": "Testo ndërprerjen me gjendje statike", "pg_stress_toggle_desc": "Simulon pajisjen që qëndron në nivelin e konsumit të gjendjes statike pas përfundimit të regjistrimit - tregon nëse dhe kur WashData ndalon ciklin.", - "pg_sweep_2d": "Hartë nxehtësie 2D", - "pg_sweep_param_y": "Parametri 2", "pg_obj_match": "Saktësia e përputhjes", "pg_obj_endtiming": "Saktësia e kohës së përfundimit", "pg_obj_falseend": "Shkalla e përfundimeve të rreme", "pg_obj_overrun": "Devijimi i kohëzgjatjes", "pg_obj_ambiguity": "Shkalla e paqartësisë", - "pg_best_combo": "kombinimi më i mirë", "pg_mode_optimize": "Optimizo", "pg_group_matching": "Përputhja e programeve", "pg_zoom_hint": "klikoni dy herë për të rivendosur zmadhimin", @@ -706,7 +719,12 @@ "task_history_import_apply": "Importim i cikleve", "evidence_real_cycles": "Ciklet që kreu kjo makinë", "evidence_reference_cycles": "Shkarkuar nga dyqani i komunitetit", - "evidence_backfill_cycles": "Gjetur në historikun e importuar të fuqisë" + "evidence_backfill_cycles": "Gjetur në historikun e importuar të fuqisë", + "zoom_hint_touch": "zmadho me dy gishta", + "zoom_hint_touch_zoomed": "zmadho me dy gishta · trokit dy herë për rivendosje", + "cycles_since_service": "{since} / {total} cikle", + "total_cycles_run": "cikle të kryera gjithsej", + "correct_total_cycles": "Korrigjo totalin" }, "log": { "all_levels": "Të gjitha nivelet", @@ -756,7 +774,7 @@ "merge_cycles": "Bashko {n} cikle", "new_group": "Grup i ri profilesh", "new_phase": "Fazë e re", - "process_history_msg": "Riekzekutoni përputhjen, rifreskoni sugjerimet, stërvitni ML (nëse aktivizuara) dhe rillogaritni shëndetin e cikleve në të gjitha ciklet e ruajtura. Kjo mund të marrë pak kohë.", + "process_history_msg": "Riekzekutoni përputhjen, rifreskoni sugjerimet, ristërvitni ML (nëse është aktivizuar), rillogaritni koston e cikleve sipas çmimeve të regjistruara të energjisë dhe rillogaritni shëndetin e cikleve në të gjitha ciklet e ruajtura. Kjo mund të marrë pak kohë.", "process_history_ok": "Proceso", "process_history_title": "Proceso historinë", "process_recording": "Proceso regjistrimin", @@ -789,6 +807,7 @@ "history_import": "Importo historikun e fuqisë" }, "msg": { + "pg_canvas_empty3": "Zgjidhni një cikël më sipër dhe shtypni Ekzekuto për ta simuluar. Pastaj kaloni kursorin për të lexuar vlerat, rrëshqitni ose zmadhoni me dy gishta dhe tërhiqni për të lëvizur.", "tail_trim_hint": "Hiqni kaq sekonda nga fundi", "store_sibling_hint": "Nuk ka asgjë të ndarë për modelin tuaj të saktë? Një model i afërt i të njëjtës markë është zakonisht një pikënisje e mirë.", "store_declare_appliance": "Përcaktoni në WashData pajisjen që zotëroni dhe kjo skedë shfaq konfigurimet që të tjerët kanë ndarë për të. Mund edhe të shkruani një markë më lart për t'i hedhur një sy katalogut.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Shkoi mbi brezin e zakonshëm të fuqisë për ~{n} s.", "auto_label_intro": "Cakto profile te ciklet e paetiketuara që besueshmëria e tyre e përputhjes tejkalon pragun.", "automations_intro": "WashData ndez ngjarje {start} / {end} dhe ekspozon entitete, kështu njoftimet dhe veprimet ndërtohen më mirë si automatizime normale të Home Assistant. Automatizimi që përdorin këtë pajisje shfaqen më poshtë.", - "cleanup_intro": "Çdo cikël i etiketuar mbivendosur. Shëno outlier-ët dhe fshi për të pastruar profilin.", + "cleanup_intro": "Çdo cikël i etiketuar është mbivendosur. Shëno outlier-ët, pastaj hiq etiketën e tyre (mbeten në historik, por nuk e formësojnë më këtë profil) ose fshiji përfundimisht.", "clear_debug_hint": "Hiqni të dhënat e ruajtura të korrigjimit për të liruar hapësirën.", "collecting_data": "Duke mbledhur të dhëna. Cikle që nevojiten para se të fillojë rregullimi i hollë: {need} ({current}/{min}).", "compare_overlay_profiles": "Mbivendos profilet (të zbetura)", @@ -901,7 +920,7 @@ "preferences_admin": "Preferencat, paneli dhe kontrolli i aksesit", "preferences_adv": "Preferencat", "prefs_personal": "Këto vlejnë vetëm për llogarinë tuaj të Asistentit në Shtëpi.", - "process_history_hint": "Riekzekutoni përputhjen në të gjitha ciklet e ruajtura, rifreskoni sugjerimet e sintonizimit, stërvitni modelet ML (nëse janë aktivizuara) dhe rillogaritni shëndetin e cikleve. Ekzekutoni këtë pas një grupi rishikimesh.", + "process_history_hint": "Riekzekutoni përputhjen në të gjitha ciklet e ruajtura, rifreskoni sugjerimet e sintonizimit, ristërvitni modelet ML (nëse janë aktivizuara), rillogaritni koston e cikleve sipas çmimeve të regjistruara të energjisë dhe rillogaritni shëndetin e cikleve. Ekzekutoni këtë pas një grupi rishikimesh.", "profile_deleted": "Profili u fshi", "profile_poor_health_detail": "Ciklet e caktuara për këtë profil kanë forma jokonsistente ose besim të ulët. Merrni parasysh rindërtimin e zarfit ose rishikimin e cikleve të etiketuara.", "profiles_intro": "Kliko një profil për statistika, faza dhe pastrim. Grupo programe pothuajse identike (të njëjtën formë/kohëzgjatje, temperaturë ose centrifugim të ndryshëm) kështu që përputhja zgjedh me besueshmëri ndërmjet tyre.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' ka një rezultat të ulët përputhjeje - ciklet e tij të fundit ndryshojnë shumë ose përputhen dobët. Rishikoni ciklet e tij ose regjistrojeni sërish profilin që njohja dhe vlerësimet e kohës të mbeten të sakta.", "advisory_shape_drift": "'{name}' ka devijuar ndjeshëm nga forma e tij fillestare e fuqisë. Sjellja e pajisjes mund të ketë ndryshuar me kalimin e kohës (p.sh. gëlqerëzim, konsumim). Merrni në konsideratë regjistrimin sërish të këtij profili me ciklet e fundit.", "advisory_shape_drift_corr": "'{name}' ka devijuar ndjeshëm nga forma e tij fillestare e fuqisë (korrelacioni {corr}). Sjellja e pajisjes mund të ketë ndryshuar me kalimin e kohës (p.sh. gëlqerëzim, konsumim). Merrni në konsideratë regjistrimin sërish të këtij profili me ciklet e fundit.", + "advisory_unmatchable": "'{name}' nuk mund të përputhet kurrë: pas tij nuk ka asnjë cikël me të dhëna të energjisë, kështu që WashData nuk ka me çfarë të krahasojë ciklin që ekzekutohet. Etiketoni një cikël të këtij programi (ose regjistroni një cikël të ri) për ta rikthyer në përputhje.", + "advisory_unmatchable_no_duration": "'{name}' nuk mund të përputhet kurrë: ka një cikël me të dhëna të energjisë, por pa një kohëzgjatje të përdorshme, kështu që WashData nuk ka asnjë kohëzgjatje mbi të cilën të masë një përputhje. Regjistrojeni sërish këtë program, ose caktoni kohëzgjatjen e tij të pritshme, për ta rikthyer në përputhje.", "pg_no_alerts": "Nuk u zbuluan probleme në këtë ekzekutim.", "pg_override_active": "Po përdoren cilësimet tuaja të redaktuara kundrejt atyre aktuale", - "pg_history_intro": "Riluani ciklet tuaja të fundit përmes zbuluesit dhe përputhësit të vërtetë. Redaktoni fillimisht një cilësim zbulimi te Simulo për të parë këtu një krahasim para/pas.", "pg_history_empty": "Shtypni Ekzekuto për të riluajtur ciklet tuaja.", "pg_history_summary": "{detected}/{total} u zbuluan · {correct} u përputhën saktë", "pg_sweep_no_metric": "Nuk ka mjaftueshëm të dhëna për të vlerësuar këtë objektiv.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Skanimi dështoi.", "hist_scan_expired": "Ai skanim nuk është më i disponueshëm. Skanoni përsëri.", "hist_import_failed": "Importimi dështoi.", - "imported_history_readonly": "Zbuluar në historikun e importuar të fuqisë. Ndikon në përputhjen e programeve, por nuk llogaritet në statistikat tuaja dhe nuk mund të shkurtohet ose të ndahet. Etiketojeni për të emërtuar programin." + "imported_history_readonly": "Zbuluar në historikun e importuar të fuqisë. Ndikon në përputhjen e programeve, por nuk llogaritet në statistikat tuaja dhe nuk mund të shkurtohet ose të ndahet. Etiketojeni për të emërtuar programin.", + "odometer_intro": "Ky total vetëm rritet. Fshirja e regjistrimit të një cikli nuk e ndryshon dhe vazhdon të numërojë përtej kufirit të historikut të ruajtur, kështu që kujtesat e servisit mbeten të sakta.", + "odometer_correct_hint": "Vendosni këtë vlerë nëse WashData regjistroi një cikël që nuk ndodhi kurrë, ose nëse pajisja kishte kryer cikle përpara se WashData të instalohej.", + "no_reminders_set": "Nuk është caktuar ende asnjë kujtesë servisi.", + "toast_program_armed": "Programi u caktua për ciklin tjetër: {program}" }, "phase_desc": { "anti_crease": "Rrotullime të shkurtra të herëpashershme pas përfundimit për të reduktuar rrudhat.", @@ -1157,6 +1181,10 @@ } }, "setting": { + "anti_crease_finalize_ratio": { + "doc": "Sa larg brenda kohëzgjatjes së pritur të programit të përputhur duhet të ketë ecur një cikël përpara se WashData të mund të vendosë se rrotullimi i qetë që po sheh është bishti kundër rrudhave dhe ta mbyllë ciklin aty. Kohëzgjatja e pritur është mesatarja e programit, prandaj te një tharëse kohëzgjatja e së cilës me sensor lagështie varet nga ngarkesa, shumica e nisjeve nuk e arrijnë kurrë 98% të saj dhe bishti qëndron deri te afati rezervë - shpesh 15 deri në 40 minuta pasi makina ka ndaluar në të vërtetë. Uleni këtë vlerë (p.sh. 0.75) te një tharëse e tillë. Makinat larëse lërini në vlerën e parazgjedhur: një makinë larëse e kalon pjesën më të madhe të ciklit nën cilësimin Fuqia maks. kundër rrudhave, prandaj ky fraksion është e vetmja gjë që ndan një fazë të qetë në mes të larjes nga bishti, dhe ulja e tij mund ta përgjysmojë një larje. Lëreni bosh për vlerën e parazgjedhur (0.98). Ky është një cilësim tjetër nga Raporti i Përfundimit të Zgjuar, i cili kontrollon një verifikim të veçantë të përfundimit të hershëm.", + "label": "Raporti i finalizimit kundër rrudhave" + }, "anti_wrinkle_enabled": { "doc": "Njihni pulset e shkurtra me fuqi të ulët që lëshon një tharëse pas fazës kryesore të nxehtësisë dhe mbajini të lidhura me ciklin e përfunduar në vend që t'i lexoni si cikle të reja.", "label": "Aktivizo zbulimin kundër rrudhave" @@ -1178,7 +1206,7 @@ "label": "Fuqia maks. kundër rrudhave" }, "auto_label_confidence": { - "doc": "Nëse rezultati i ndeshjes në fund të ciklit është në ose mbi këtë, programi etiketohet automatikisht pa ndonjë kërkesë konfirmimi. Rriteni atë për të kërkuar siguri më të lartë përpara etiketimit automatik; uleni atë për të automatizuar më shumë. Funksionon në lidhje me besimin e të mësuarit nën të.", + "doc": "Nëse rezultati i përputhjes në fund të ciklit është në këtë nivel ose mbi të, programi etiketohet automatikisht pa asnjë kërkesë konfirmimi. Rriteni për të kërkuar siguri më të lartë përpara etiketimit automatik; uleni për të automatizuar më shumë. Funksionon së bashku me Besueshmërinë e të mësuarit menjëherë poshtë tij.", "label": "Besueshmëria e etiketimit automatik" }, "auto_maintenance": { @@ -1189,6 +1217,10 @@ "doc": "Ciklet më të shkurtra se kjo hidhen si cikle fantazmë (vrapime testimi, hapja e derës për të shtuar një çorape).", "label": "Kohëzgjatja min. e ciklit" }, + "curve_preroll_seconds": { + "doc": "Sa sekonda leximesh nga përpjekjet e ndërprera për nisje mund të bartë WashData në fillim të kurbës së një cikli. Kurba e një cikli normalisht fillon nga përpjekja e nisjes që më në fund ia del, prandaj te një makinë që provon disa herë përpara se të vendoset - zgjedhja e programit, bllokimi i derës, mbushja e parë me ujë - minutat e para të aktivitetit të vërtetë mungojnë në çdo kurbë. Vendoseni këtë vlerë pak mbi nisjen më të gjatë të tillë që ka makina juaj (300 s është më se e mjaftueshme për shumicën). 0 e çaktivizon. Kini parasysh se aktivizimi e zhvendos më herët fillimin e regjistruar, prandaj ciklet e regjistruara para dhe pas ndryshimit mbajnë kohëzgjatje të ndryshme për të njëjtin program; mesataret e mësuara do të zhvendosen derisa ciklet më të vjetra të dalin nga përdorimi.", + "label": "Sekondat hyrëse të kurbës" + }, "delay_confirm_seconds": { "doc": "Energjia duhet të qëndrojë në brezin e gatishmërisë për kaq shumë kohë përpara se pajisja të trajtohet si në pritje për të nisur në vend që të funksionojë.", "label": "Sekonda konfirmimi" @@ -1226,7 +1258,7 @@ "label": "Numri i përsëritjeve të fundit" }, "energy_price_entity": { - "doc": "Sensori me çmimin aktual të energjisë elektrike për kWh (p.sh. një tarifë dinamike). Ka përparësi ndaj çmimit statik më poshtë. Çdo cikël ngrin çmimin në fuqi kur të përfundojë.", + "doc": "Sensori me çmimin aktual të energjisë elektrike për kWh (p.sh. një tarifë dinamike). Duhet të jetë një çmim, jo entiteti i fuqisë ose i energjisë së vetë prizës: ka përparësi ndaj çmimit statik më poshtë, prandaj një numërues kWh i vendosur këtu e faturon çdo cikël me leximin e matësit në vend të tarifës suaj. Kur është aktive Kosto e ponderuar sipas kohës, çdo cikël faturohet me çmimin në fuqi në çdo çast që ai punoi; përndryshe mbi të ngrihet çmimi në fuqi kur ai përfundoi.", "label": "Entiteta e çmimit të energjisë" }, "energy_price_static": { @@ -1238,7 +1270,7 @@ "label": "Entiteti i matësit të energjisë" }, "expose_debug_entities": { - "doc": "Publikoni entitete shtesë diagnostikuese HA (besimi i ndeshjes, paqartësia, të brendshmet e gjendjes). Off e mban listën e entiteteve të pastër për përdorim normal.", + "doc": "Publikoni entitete shtesë diagnostikuese HA (besueshmëria e përputhjes, paqartësia, të brendshmet e gjendjes). Joaktiv e mban listën e entiteteve të pastër për përdorim normal.", "label": "Ekspono entitetet e korrigjimit" }, "external_end_trigger": { @@ -1254,7 +1286,7 @@ "label": "Invertoj aktivizuesin e jashtëm (aktivizoj kur FIKUR)" }, "learning_confidence": { - "doc": "Nëse rezultati i ndeshjes bie midis kësaj dhe \"Konfidencës me etiketë automatike\", një njoftim reagimi ju kërkon të verifikoni programin e identifikuar. Poshtë këtij rezultati ndeshja është shumë e pasigurt për t'u shfaqur. Duhet të mbahet nën Besueshmërinë e Etiketës Automatike.", + "doc": "Nëse rezultati i përputhjes bie midis kësaj dhe Besueshmërisë së etiketimit automatik, një njoftim reagimi ju kërkon të verifikoni programin e identifikuar. Poshtë këtij rezultati përputhja është shumë e pasigurt për t'u shfaqur. Duhet të mbahet nën Besueshmërinë e etiketimit automatik.", "label": "Besueshmëria e të mësuarit" }, "linked_device": { @@ -1322,7 +1354,7 @@ "label": "Aktivizo ngjarje HA për njoftime" }, "notify_icon": { - "doc": "Ikona opsionale mdi për njoftimin (p.sh. mdi: makinë larëse). Lëreni bosh për parazgjedhjen e platformës.", + "doc": "Ikona e vogël që shfaqet në njoftim (p.sh. mdi:washing-machine). Android e vizaton në shiritin e statusit; iOS e shfaq në vend të ikonës së aplikacionit dhe kërkon aplikacionin shoqërues 2026.8 ose më të ri. Lëreni bosh për parazgjedhjen e platformës.", "label": "Ikona e njoftimit" }, "notify_live_chronometer": { @@ -1422,7 +1454,7 @@ "label": "Pragu i përputhjes" }, "profile_unmatch_threshold": { - "doc": "Nëse një ndeshje e drejtpërdrejtë në mes të ciklit bie nën këtë rezultat, identifikimi paraprak pastrohet. Mbajeni atë pak më poshtë Pragut të Përputhjes në mënyrë që një rënie e shkurtër në ngjashmëri të mos e kthejë ekranin përsëri në të pashoq.", + "doc": "Nëse një përputhje e drejtpërdrejtë në mes të ciklit bie nën këtë rezultat, identifikimi paraprak pastrohet. Mbajeni pak më poshtë Pragut të përputhjes, në mënyrë që një rënie e shkurtër e ngjashmërisë të mos e kthejë ekranin përsëri në gjendjen pa përputhje.", "label": "Pragu i mosperputhjeve" }, "progress_reset_delay": { @@ -1433,10 +1465,6 @@ "doc": "Sekonda një pompë mund të funksionojë vazhdimisht përpara se të shënohet si e mundshme e mbërthyer (ndiz ngjarjen e pompës së mbërthyer).", "label": "Kohëzgjatja e pompës së bllokuar" }, - "running_dead_zone": { - "doc": "Pas fillimit të një cikli, uljet e energjisë brenda kësaj dritareje nuk merren parasysh. Makinat larëse mbushen me ujë të ftohtë (duke rënë afër 0 W para ngrohjes) - pa këtë mbrojtje, faza e mbushjes duket si një fund cikli. Kjo NUK kapërcen të dhënat: gjurma e plotë e fuqisë regjistrohet nga T=0. Motori i sugjerimeve mat modelin aktual të nisjes së makinës suaj dhe e përmason atë automatikisht.", - "label": "Zona e vdekur e ekzekutimit" - }, "sampling_interval": { "doc": "Koha e pritshme midis leximeve të sensorit - përdoret për madhësinë e dritares së zbutjes dhe fillimin e debouncimit në mënyrë korrekte. Çdo përditësim i sensorit kapet pavarësisht nga kjo vlerë; ai kalibron vetëm llogaritjet e rrjedhës së poshtme. Motori i sugjerimeve mat kadencën aktuale të sensorit tuaj nga ciklet e kaluara dhe e vendos këtë automatikisht.", "label": "Intervali i kampionimit" @@ -1564,13 +1592,17 @@ "label": "Intervali i Profilit të Energjisë", "doc": "Madhësia e kovës për atributin e sensorit power_profile (vargu i sheshtë me vat mesatare për slot) të konsumuar nga planifikues të jashtëm si EMHASS dhe tibber_prices. Kova më të vogla mbajnë pikat e shkurtra të energjisë të mprehta; kova më të mëdha zbuten forma. Parazgjedhja 15 min. Vetëm për lexim; nuk ndikon zbulimin." }, + "notify_live_silent": { + "label": "Përditësime live pa zë", + "doc": "Vetëm iOS. Rifreskoni progresin live në heshtje: çdo përditësim vjen pa zë dhe pa dridhje, me një përparësi më të ulët që kursen baterinë dhe që iOS mund ta grupojë për pak kohë. Përditësimi i parë i një cikli (ai që nis Live Activity), si dhe njoftimet e nisjes dhe të përfundimit, sinjalizojnë si zakonisht. Çaktivizojeni këtë për t'u njoftuar në çdo përditësim." + }, "notify_live_sticky": { "label": "Mbaj njoftimin live pas trokitjes", "doc": "Vetëm Android. Bëni njoftimin e progresit live të vazhdueshëm (sticky) kështu që trokitja mbi të nuk e humb fijen e vazhdueshme. Off ruan sjelljen e parazgjedhur ku trokitja e humb atë." }, "notify_live_click_action": { - "label": "Destinacioni i trokitjes së njoftimit live", - "doc": "Vetëm Android. Ku hap trokitja mbi njoftimin e progresit live (p.sh. /lovelace/laundry ose një URL e plotë) në vend të faqes kryesore të aplikacionit. Lëreni bosh për parazgjedhjen." + "label": "Destinacioni i trokitjes së njoftimit", + "doc": "Ku të çojë trokitja mbi një njoftim nga kjo pajisje, në Android dhe iOS. E lënë bosh, hap panelin WashData te kjo pajisje. Shkruani shtegun e një paneli kontrolli (p.sh. /lovelace/laundry) ose një URL të plotë për ta dërguar diku tjetër, ose \"none\" që të mos ketë fare destinacion trokitjeje." }, "door_opens_at_end": { "label": "Dera hapet automatikisht në fund", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Ciklet që formojnë një program", "doc": "Cilat cikle përdoren për të ndërtuar kurbën e fuqisë së secilit program dhe për të përputhur me të një cikël të përfunduar. Heqja e shenjës nga një lloj e ndalon atë të formojë programet tuaja pa fshirë asgjë - ciklet mbeten në listën Cikle dhe mund të etiketohen ose të hiqen si më parë. E dobishme nëse nuk i besoni të dhënave të importuara. Statistikat nuk preken: ato numërojnë gjithmonë vetëm ciklet që kjo makinë kreu vërtet. Heqja e shenjës nga të gjitha llojet shpërfillet, sepse një program pa cikle pas tij nuk mund të përputhej kurrë." + }, + "energy_price_dynamic": { + "label": "Kosto e ponderuar sipas kohës", + "doc": "Çdo cikël faturohet me çmimin në fuqi në çdo çast që ai punoi, në vend të një çmimi të vetëm në fuqi kur ai përfundoi. Vlen vetëm për një entitet çmimi (një çmim statik nuk ndryshon). Nuk kërkon konfigurim shtesë: çmimi ndiqet drejtpërdrejt ndërsa cikli po punon, kurse veprimi Proceso historinë te Diagnostikimi rillogarit koston e cikleve më të vjetra nga recorder. Joaktiv = sjellja klasike, një çmim i vetëm i ngrirë në fund." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Po ndahet…", "deleting": "Po fshihet…", "imported": "Importuar", - "preparing": "Po përgatitet…" + "preparing": "Po përgatitet…", + "unlabelling": "Po hiqet etiketa…" }, "tab": { "advanced": "I avancuar", @@ -1778,6 +1815,7 @@ "recording_stopped": "Regjistrimi u ndalua", "review_saved": "Rishikimi u ruajt", "settings_conflicts": "Korrigjoni konfliktet e theksuara të cilësimeve para ruajtjes.", + "settings_no_changes": "Nuk ka ndryshime për të ruajtur", "settings_reverted": "Cilësimet u rivendosën; integrimi po ringarkohet", "settings_saved": "Cilësimet u ruajtën; integrimi po ringarkohet", "suggestions_applied": "Sugjerimet u zbatuan; integrimi po ringarkohet", @@ -1836,7 +1874,11 @@ "export_selective_done": "Eksporti u shkarkua", "import_selective_done": "U importuan {profiles} profil(e) dhe {cycles} cikël(cikle)", "hist_csv_required": "Ngarkoni së pari një skedar CSV ose ngjitni përmbajtjen e tij", - "file_read_failed": "Skedari nuk mund të lexohej" + "file_read_failed": "Skedari nuk mund të lexohej", + "unlabel_done": "U hoq etiketa nga {count} cikël(e)", + "unlabel_failed": "Heqja e etiketës dështoi: {error}", + "odometer_saved": "Totali i cikleve u ruajt", + "odometer_save_failed": "Nuk u ruajt dot totali: {error}" }, "suggestion": { "both_agree": "WashData rekomandon", @@ -1883,10 +1925,6 @@ "higher": "Konfirmon vetëm përputhjet me besueshmëri të lartë; më shumë cikle mbeten pa etiketë", "lower": "Përputhet me më shumë cikle; disa mund të jenë paksa të gabuara" }, - "running_dead_zone": { - "higher": "Injoron kumbëzat e shkurtra të fuqisë gjatë pushimit – më pak e ndjeshme ndaj zhurmës", - "lower": "Reagron ndaj shpërthimeve më të shkurtra të fuqisë – kap aktivitetin kalimtar të shpejtë" - }, "start_threshold_w": { "higher": "Shmang startet false nga kumbëzat e shkurtra të fuqisë", "lower": "Kap programet me fuqi të ulët; mund të aktivizohet nga zhurma" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} cikle të pastra ({pct}%) patën një pauzë të brendshme >60s që vazhdoi; kërkohen {val} konfirmim(e) fundi.", "learning_confidence": "Besueshmëria p05 e {cycles} cikleve të etiketuara nga përdoruesi ({p05}); nën këtë vlerë, kërkohet verifikim.", "match_interval": "Bazuar në ritmin e vëzhguar të përditësimit (mesatarja={median}s) * 10.", + "match_interval_capped": "Kufizuar që {persistence} përputhje të njëpasnjëshme të hyjnë në {pct}% të programit më të shkurtër ({shortest}s); vetëm ritmi i përditësimit (mesatarja={median}s) do të kishte sugjeruar një interval më të gjatë.", + "match_interval_floored": "Vetëm programi më i shkurtër ({shortest}s) do ta kufizonte këtë në {cap}s, nën minimumin prej {minimum}s, prandaj mbahet te minimumi: {persistence} përputhje të njëpasnjëshme zgjasin {budget}s, që është më shumë se {pct}% e atij programi. Përputhja kryhet vetëm kur mbërrin një lexim i ri (mesatarja={median}s), ndaj minimumi nuk kushton asgjë.", "max_duration_ratio": "Bazuar në kohëzgjatjet e cikleve të etiketuara (p95={p95}).", "min_active": "Bazuar në fuqinë minimale aktive ({min}W) të vëzhguar në ciklin e fundit.", "min_duration_ratio": "Mbahet sa më i ulët që një program të njihet herët në cikël; pragjet e besueshmërisë dhe të paqartësisë parandalojnë konfirmimet e parakohshme.", @@ -1990,7 +2030,9 @@ "duration_scale": "Etapa 4: raporti logaritmik ku marrëveshja e kohëzgjatjes gjysmohet; më i vogël = ndëshkim më i rreptë (parazgjedhja 0.175)", "energy_scale": "Etapa 4: raporti logaritmik ku marrëveshja e energjisë gjysmohet; më i vogël = ndëshkim më i rreptë (parazgjedhja 0.25)", "dishwasher_end_spike_quiet_release": "Pjatalarëse: sekonda qetësie pas kohëzgjatjes së pritur para se të lirohet pritja e shkarkimit në fund të ciklit", - "smart_termination_duration_ratio": "Pjesa e kohëzgjatjes së pritur të programit të përputhur që një cikël duhet të arrijë përpara se Përfundimi i Zgjuar të mund ta mbyllë atë herët; uleni për makina që varen nga ngarkesa ose temperatura" + "smart_termination_duration_ratio": "Pjesa e kohëzgjatjes së pritur të programit të përputhur që një cikël duhet të arrijë përpara se Përfundimi i Zgjuar të mund ta mbyllë atë herët; uleni për makina që varen nga ngarkesa ose temperatura", + "anti_crease_finalize_ratio": "Pjesa e kohëzgjatjes së pritur të programit të përputhur që një cikël duhet të arrijë përpara se bishti i rrotullimit kundër rrudhave të mund të finalizohet; uleni te tharëset me bisht të gjatë rrotullimi, makinat larëse lërini në vlerën e parazgjedhur", + "curve_preroll_seconds": "Sekonda leximesh nga përpjekjet e ndërprera për nisje të bartura në fillim të kurbës së një cikli; 0 = joaktiv" }, "col": { "profile_tip": "Emri i programit të përputhur. I paetiketuar do të thotë se asnjë profil nuk u përputh në fund të ciklit.", @@ -1998,10 +2040,11 @@ "date_tip": "Data dhe ora kur filloi cikli.", "duration_tip": "Koha e plotë e ekzekutimit të ciklit nga fillimi deri në fund.", "energy_tip": "Energjia e plotë e konsumuar (kWh). Llogaritur duke integruar fuqinë me kalimin e kohës.", - "cost_tip": "Kostoja e energjisë për këtë cikël, e ngrirë në përfundim duke përdorur çmimin në fuqi atëherë (energjia x çmimi për kWh). Vendos një çmim te Cilësimet për ta plotësuar.", + "cost_tip": "Kostoja e energjisë për këtë cikël, e ngrirë në përfundim. Kur është aktive Kosto e ponderuar sipas kohës, kurba e fuqisë faturohet me çmimin në fuqi në çdo çast; përndryshe energjia x çmimi i vetëm në fuqi në fund. Vendos një çmim te Cilësimet për ta plotësuar.", "confidence_tip": "Besueshmëria e përputhjes së profilit (0-100%). Sa afër përputhej kurba e fuqisë së ciklit me programin e identifikuar.", "health_tip": "Shëndeti ML i ciklit (më lart = më mirë). Kliko një cikël për ta inspektuar dhe shqyrtuar.", - "flags_tip": "Flamujt e rishikimit, anomalive dhe burimit për ciklin. Vendosni miun mbi ikonë për detaje." + "flags_tip": "Flamujt e rishikimit, anomalive dhe burimit për ciklin. Vendosni miun mbi ikonë për detaje.", + "cost_dynamic_tip": "E ponderuar sipas kohës: e faturuar me çmimin në fuqi gjatë ciklit (efektivisht {price} {cur}/kWh)." }, "stat": { "avg": "mesatare", @@ -2028,9 +2071,9 @@ "finished": "Cikli arriti gjendjen përfundimtare dhe përfundoi." }, "store": { + "search_ph2": "Kërko sipas markës ose modelit…", "your_model_tip": "Kjo është pajisja që deklaruat te Cilësimet", "your_model": "E juaja", - "search_brand_ph": "Kërko sipas markës…", "programs_count": "Programe: {n}", "browse": "Shfleto", "device": "Pajisja", @@ -2076,7 +2119,8 @@ "golden": "Ripërpunim: shënimi i cikleve referues", "suggestions": "Ripërpunim: sugjerime", "ml_training": "Ripërpunim: trajnimi i ML-së", - "health": "Ripërpunim: shëndeti i cikleve" + "health": "Ripërpunim: shëndeti i cikleve", + "costs": "Ripërpunim: kostot e energjisë" }, "pg_sweep": { "optimize": "Optimizim: {param}" diff --git a/custom_components/ha_washdata/translations/panel/sr-Latn.json b/custom_components/ha_washdata/translations/panel/sr-Latn.json index 71a3326f..c314a060 100644 --- a/custom_components/ha_washdata/translations/panel/sr-Latn.json +++ b/custom_components/ha_washdata/translations/panel/sr-Latn.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Pri kraju ovaj program utihne na oko {mins} min, zatim povuče oko {watts} W tokom {secs} s i završava. Uočeno u {seen}/{measured} izmerenih ciklusa: uređaj to ne radi pri svakom pokretanju.", + "quiet_tail": "~{mins} min tišine na kraju", "artifact_tip": "Otkriveno je {n} anomalija (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", "auto": "(automatski otkriveno)", "built_in_tag": "ugrađen", @@ -77,11 +79,14 @@ "approved": "Odobreno", "awaiting": "Čeka odobrenje", "imported_tip": "Uvezeno iz prodavnice zajednice. Koristi se samo za podudaranje, ne računa se u statistiku.", + "unmatchable": "ne može se podudariti", "not_importable": "nije primenljivo", "exists": "postoji", - "backfilled_tip": "Detektovano u uvezenoj istoriji snage. Utiče samo na podudaranje programa, ne računa se u statistiku." + "backfilled_tip": "Detektovano u uvezenoj istoriji snage. Utiče samo na podudaranje programa, ne računa se u statistiku.", + "armed": "(primenjuje se na sledeći ciklus)" }, "btn": { + "reset_zoom": "Poništi zumiranje", "set_brand_model": "Postavi marku i model", "refresh_catalog": "Osveži katalog", "add_device": "+ Dodaj uređaj", @@ -217,6 +222,8 @@ "trim": "Iseci", "trim_split": "Iseci / Podeli", "undo": "Opozovi", + "unlabel_selected": "Ukloni oznaku sa izabranih ({n})", + "unlabel_selected_tip": "Uklanja oznaku profila sa izabranih ciklusa. Ostaju u vašoj istoriji i mogu ponovo da se označe kasnije - samo prestaju da oblikuju ovaj profil.", "use": "Koristi", "wipe_all": "Obriši sve podatke", "delete_selected": "Izbriši izabrano ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Primeni najbolje", "pg_apply_to_settings": "Sačuvaj u podešavanja", "pg_apply_to_settings_tip": "Kopiraj ovde uređene vrednosti u aktivna podešavanja ovog uređaja", - "search": "Pretraži", "import": "Uvezi", "disconnect": "Prekini vezu", "connect_github": "Poveži GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Pročitaj iz Home Assistant", "hist_scan": "Potraži cikluse", "hist_import_n": "Uvezi cikluse: {n}", - "hist_goto_cycles": "Prikaži cikluse" + "hist_goto_cycles": "Prikaži cikluse", + "save_total_cycles": "Sačuvaj ukupan broj" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Mora biti iznad Praga isključivanja napajanja ({pot} W)", "threshold": "Mora biti ispod Praga zaustavljanja ({stop} W) da bi imalo efekta" }, + "price_entity": { + "power_sensor": "Ovo je Senzor snage uređaja, a ne cena. Entitet cene ima prednost nad statičnom cenom ispod, pa bi ciklusi bili obračunati po vatima za kWh - obrišite ovo polje.", + "energy_sensor": "Ovo je Brojilo energije uređaja, a ne cena. Entitet cene ima prednost nad statičnom cenom ispod, pa bi ciklusi bili obračunati po očitavanju brojila za kWh - obrišite ovo polje.", + "device_class": "Klasa ovog senzora je {dc}, a ne cena po kWh. Entitet cene ima prednost nad statičnom cenom ispod - obrišite ovo polje ili ga usmerite na senzor tarife.", + "unit": "Ovaj senzor očitava u {unit}, što nije cena po kWh. Entitet cene ima prednost nad statičnom cenom ispod - obrišite ovo polje ili ga usmerite na senzor tarife." + }, "pump_stuck": { "duration": "Mora biti manji od Vremenskog ograničenja bez ažuriranja ({to} s) kako bi alarm zaglavljene pumpe okidao pre nego nadzornik prekine ciklus", "timeout": "Mora premašiti Trajanje zaglavljene pumpe ({ps} s) kako bi alarm okidao pre prisilnog zaustavljanja ciklusa" @@ -349,7 +362,7 @@ "pg_settings_source": "Izvor podešavanja", "phase_catalog": "Katalog faza", "playground": "Testno okruženje", - "process_history": "Istorija procesa", + "process_history": "Obrada istorije", "settings_history": "Istorija podešavanja", "status": "Status", "status_graph": "Grafikon statusa", @@ -360,12 +373,12 @@ "wipe_history": "Obriši istoriju", "recommendations": "Preporuke ({n})", "pg_detection_params": "Podešavanja otkrivanja", - "pg_timeline": "Vremenska osa događaja", "pg_outcome": "Ishod simulacije", "pg_across_cycles": "Kroz sve vaše cikluse", "community_store": "Prodavnica zajednice", "online_account": "Prodavnica zajednice i online funkcije", - "import_power_history": "Uvoz istorije snage" + "import_power_history": "Uvoz istorije snage", + "service_status": "Status servisa" }, "health": { "fair": "Prihvatljiva kvaliteta profila", @@ -373,6 +386,10 @@ "poor": "⚠ Loša kvaliteta profila" }, "lbl": { + "envelope_position_tip": "Položaj na sopstvenoj snimljenoj krivoj podudarenog programa, {pct}%. Meri se poravnanjem ovog ciklusa sa profilom umesto brojanjem vremena, pa ostaje tačan kada je pokretanje duže ili kraće nego obično. Osvežava se dok je uređaj tih, pa tokom aktivne faze može da kasni.", + "envelope_position": "kriva {pct}%", + "aria_playground_chart3": "Interaktivni grafikon snage ciklusa: pređite mišem za prikaz vremena/snage, pomerajte točkić ili koristite dva prsta za zumiranje, prevucite za pomeranje", + "device_unresolved": "Nedostupan uređaj ({id})", "drag_to_resize": "Prevucite za promenu veličine", "actions": "Akcije", "activity": "Aktivnost", @@ -509,7 +526,7 @@ "profile_index": "indeks profila →", "profile_name": "Naziv profila", "program": "Program", - "program_tip": "Zamenite profil koji se poklapa sa trenutnim ciklusom. Automatsko otkrivanje dozvoljava integraciji da automatski izabere najbolje poklapanje. Zakačite određeni program da biste prinudno poklopili kada automatsko otkrivanje nije ispravno ili znate šta se pokreće.", + "program_tip": "Zamenite profil koji se poklapa sa trenutnim ciklusom. Automatsko otkrivanje dozvoljava integraciji da automatski izabere najbolje poklapanje. Zakačite određeni program da biste prinudno poklopili kada automatsko otkrivanje nije ispravno ili znate šta se pokreće. Izaberite program pre pokretanja uređaja i primeniće se čim počne sledeći ciklus.", "progress": "Napredak", "raw_socket": "Sirovi priključak", "read_only": "Samo za čitanje", @@ -544,6 +561,7 @@ "stage4": "Etapa 4 – slaganje", "status": "Status", "tail_trim": "Isecanje kraja (s)", + "tap_to_dismiss": "dodirnite grafikon da zatvorite", "timer_auto_pause": "Automatska pauza", "timer_min": "min", "timer_msg_placeholder": "Poruka (opciono, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Preostalo vreme (model)", "pg_time_left_model_tip": "Procena preostalog vremena modelom (procenjivač faza + ML), a ne statično odbrojavanje", "phase": "Faza", - "pg_mode_simulate": "Simulacija", "pg_mode_history": "Test na istoriji", - "pg_mode_sweep": "Pretraga", "pg_ev_detected": "Otkriveno", "pg_ev_match": "Poklapanje prihvaćeno", "pg_ev_match_changed": "Poklapanje promenjeno", @@ -612,14 +628,11 @@ "pg_sweep_param": "Parametar", "pg_objective": "Cilj", "pg_steps": "Koraci", - "pg_sweep_2d": "2D toplotna mapa", - "pg_sweep_param_y": "2. parametar", "pg_obj_match": "Tačnost poklapanja", "pg_obj_endtiming": "Tačnost završetka", "pg_obj_falseend": "Stopa lažnih završetaka", "pg_obj_overrun": "Odstupanje trajanja", "pg_obj_ambiguity": "Stopa dvosmislenosti", - "pg_best_combo": "najbolja kombinacija", "pg_mode_optimize": "Optimizuj", "pg_group_matching": "Podudaranje programa", "pg_zoom_hint": "dvostruki klik poništava zumiranje", @@ -706,7 +719,12 @@ "task_history_import_apply": "Uvoz ciklusa", "evidence_real_cycles": "Ciklusi koje je ovaj uređaj obavio", "evidence_reference_cycles": "Preuzeti iz prodavnice zajednice", - "evidence_backfill_cycles": "Pronađeni u uvezenoj istoriji snage" + "evidence_backfill_cycles": "Pronađeni u uvezenoj istoriji snage", + "zoom_hint_touch": "zumirajte sa dva prsta", + "zoom_hint_touch_zoomed": "zumirajte sa dva prsta · dvostruki dodir za resetovanje", + "cycles_since_service": "{since} / {total} ciklusa", + "total_cycles_run": "ciklusa ukupno", + "correct_total_cycles": "Ispravi ukupan broj" }, "log": { "all_levels": "Svi nivoi", @@ -756,7 +774,7 @@ "merge_cycles": "Spoji {n} ciklusa", "new_group": "Nova grupa profila", "new_phase": "Nova faza", - "process_history_msg": "Ponovo pokrenite uparivanje, osvežite predloge, trenirajte ML (ako je omogućeno) i ponovo izračunajte zdravlje ciklusa za sve sačuvane cikluse. Ovo može potrajati.", + "process_history_msg": "Ponovo pokrenite podudaranje, osvežite predloge, ponovo trenirajte ML (ako je omogućeno), ponovo izračunajte troškove prema zabeleženim cenama energije i ponovo izračunajte zdravlje ciklusa za sve sačuvane cikluse. Ovo može potrajati.", "process_history_ok": "Obradi", "process_history_title": "Obradi istoriju", "process_recording": "Obradi snimak", @@ -789,6 +807,7 @@ "history_import": "Uvoz istorije snage" }, "msg": { + "pg_canvas_empty3": "Izaberite ciklus iznad i pritisnite Pokreni da ga simulirate. Zatim pređite mišem za prikaz vrednosti, pomerajte točkić ili koristite dva prsta za zumiranje i prevucite za pomeranje.", "tail_trim_hint": "Uklonite ovoliko sekundi sa kraja", "store_sibling_hint": "Nema ničega podeljenog za vaš tačan model? Blisko srodan model iste marke obično je dobra početna tačka.", "store_declare_appliance": "Recite integraciji WashData koji uređaj posedujete i ova kartica će prikazati konfiguracije koje su drugi podelili za njega. Možete i da upišete marku iznad da biste pregledali katalog.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Prešao uobičajeni opseg snage za ~{n} s.", "auto_label_intro": "Dodeli profile neoznačenim ciklusima čija pouzdanost podudaranja prelazi prag.", "automations_intro": "WashData okida događaje {start} / {end} i izlaže entitete, pa je obaveštenja i akcije najbolje graditi kao normalne automatizacije Home Assistanta. Automatizacije koje koriste ovaj uređaj prikazane su ispod.", - "cleanup_intro": "Svaki označeni ciklus je prekriven. Označi outlier-e i izbriši za čišćenje profila.", + "cleanup_intro": "Svaki označeni ciklus je prikazan preklopljeno. Označi odstupanja, pa im ukloni oznaku (ostaju u istoriji, ali više ne oblikuju ovaj profil) ili ih izbriši u potpunosti.", "clear_debug_hint": "Uklonite sačuvane podatke za otklanjanje grešaka da biste oslobodili prostor.", "collecting_data": "Prikupljanje podataka. Još potrebnih ciklusa pre početka finog podešavanja: {need} ({current}/{min}).", "compare_overlay_profiles": "Preklapanje profila (bledi)", @@ -902,7 +921,7 @@ "preferences_adv": "Preference", "prefs_personal": "Ovo se odnosi samo na vaš Home Assistant nalog.", "font_size_hint": "Povećajte ili smanjite sve u ovom panelu. Primenjuje se na vaš nalog na ovom uređaju.", - "process_history_hint": "Ponovo pokrenite uparivanje na svim sačuvanim ciklusima, osvežite predloge podešavanja, ponovo trenirajte ML modele (ako su omogućeni) i ponovo izračunajte zdravlje ciklusa. Pokrenite nakon niza recenzija.", + "process_history_hint": "Ponovo pokrenite podudaranje na svim sačuvanim ciklusima, osvežite predloge podešavanja, ponovo trenirajte ML modele (ako su omogućeni), ponovo izračunajte troškove prema zabeleženim cenama energije i ponovo izračunajte zdravlje ciklusa. Pokrenite ovo nakon niza pregleda.", "profile_deleted": "Profil obrisan", "profile_poor_health_detail": "Ciklusi koji su dodeljeni ovom profilu imaju nedosledne oblike ili nisku pouzdanost. Razmislite o ponovnoj izgradnji omotača ili pregledu označenih ciklusa.", "profiles_intro": "Klikni na profil za statistike, faze i čišćenje. Grupiši skoro identične programe (isti oblik/trajanje, različita temperatura ili centrifugiranje) kako bi podudaranje pouzdano biralo između njih.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "Profil '{name}' ima nisku ocenu podudaranja - njegovi noviji ciklusi znatno variraju ili se slabo podudaraju. Pregledajte njegove cikluse ili ponovo snimite profil kako bi prepoznavanje i procene vremena ostali tačni.", "advisory_shape_drift": "Profil '{name}' je znatno odstupio od svog prvobitnog oblika potrošnje snage. Uređaj je možda vremenom promenio ponašanje (npr. kamenac, habanje). Razmislite o ponovnom snimanju ovog profila sa novijim ciklusima.", "advisory_shape_drift_corr": "Profil '{name}' je znatno odstupio od svog prvobitnog oblika potrošnje snage (korelacija {corr}). Uređaj je možda vremenom promenio ponašanje (npr. kamenac, habanje). Razmislite o ponovnom snimanju ovog profila sa novijim ciklusima.", + "advisory_unmatchable": "Profil '{name}' nikada se ne može podudariti: iza njega ne stoji nijedan ciklus sa podacima o snazi, pa WashData nema sa čime da uporedi ciklus koji je u toku. Označite ciklus ovog programa (ili snimite novi) kako bi se ponovo uključio u podudaranje.", + "advisory_unmatchable_no_duration": "Profil '{name}' nikada se ne može podudariti: ima ciklus sa podacima o snazi, ali bez upotrebljivog trajanja, pa WashData nema referentnu dužinu prema kojoj bi se podudaranje merilo. Snimite ovaj program ponovo ili postavite njegovo očekivano trajanje kako bi se vratio u podudaranje.", "pg_no_alerts": "U ovom pokretanju nisu otkriveni problemi.", "pg_override_active": "Koriste se vaša izmenjena podešavanja umesto trenutnih", - "pg_history_intro": "Ponovo pokrenite svoje nedavne cikluse kroz stvarni detektor i uparivač. Prvo izmenite podešavanje otkrivanja u „Simulaciji“ da biste ovde videli poređenje pre/posle.", "pg_history_empty": "Pritisnite „Pokreni“ da biste reprodukovali svoje cikluse.", "pg_history_summary": "{detected}/{total} otkriveno · {correct} ispravno upareno", "pg_sweep_no_metric": "Nema dovoljno podataka za ocenu ovog cilja.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Pretraživanje nije uspelo.", "hist_scan_expired": "To pretraživanje više nije dostupno. Pokrenite ga ponovo.", "hist_import_failed": "Uvoz nije uspeo.", - "imported_history_readonly": "Detektovano u uvezenoj istoriji snage. Utiče na podudaranje programa, ali se ne računa u vašu statistiku i ne može se isecati ni deliti. Označite ga da mu dodelite program." + "imported_history_readonly": "Detektovano u uvezenoj istoriji snage. Utiče na podudaranje programa, ali se ne računa u vašu statistiku i ne može se isecati ni deliti. Označite ga da mu dodelite program.", + "odometer_intro": "Ovaj ukupan broj samo raste. Brisanje zapisa o ciklusu ga ne menja i nastavlja da broji i preko ograničenja sačuvane istorije, tako da podsetnici za servis ostaju tačni.", + "odometer_correct_hint": "Postavite ovo ako je WashData zabeležio ciklus koji se nikada nije dogodio ili ako je uređaj već obavio cikluse pre instaliranja WashData.", + "no_reminders_set": "Još nema postavljenih podsetnika za servis.", + "toast_program_armed": "Program podešen za sledeći ciklus: {program}" }, "phase_desc": { "anti_crease": "Povremena kratka okretanja nakon završetka radi smanjenja gužvanja.", @@ -1161,6 +1185,10 @@ "label": "Otpuštanje nakon tihe faze sušenja", "doc": "Kada ciklus pređe svoje očekivano trajanje, koliko dugo mašina mora ostati tiha (ispod Praga zaustavljanja) pre nego što WashData prestane da čeka na završno ispumpavanje vode i završi ciklus. Povećajte ako vaš uređaj ima dugu tihu fazu sušenja pre kasnog završnog ispumpavanja koje se propušta - šire okno omogućava da naučeno trajanje prati sezonske promene (hladnija dovodna voda = duži ciklusi) umesto da ostane zaključano na staroj prosečnoj vrednosti. Čekanje samo skraćuje u odnosu na interno ograničenje završnog ispumpavanja od 30 minuta, nikada ga ne produžava." }, + "anti_crease_finalize_ratio": { + "doc": "Koliko duboko u očekivano trajanje podudarenog programa ciklus mora da odmakne pre nego što WashData sme da zaključi da je tiho prevrtanje bubnja koje vidi završni rep zaštite od gužvanja i da tu okonča ciklus. Očekivano trajanje je prosek programa, pa kod mašine za sušenje čije vreme rada sa senzorom vlage prati količinu veša većina ciklusa nikada ne dostigne 98% tog proseka, pa rep stoji sve do rezervnog tajmauta - često 15 do 40 minuta nakon što je mašina zaista stala. Kod takve mašine za sušenje smanjite ovu vrednost (npr. 0.75). Mašine za pranje veša ostavite na podrazumevanoj vrednosti: mašina za pranje veći deo ciklusa provede ispod podešavanja Maks. snaga zaštite od gužvanja, pa je ovaj udeo jedino što razdvaja tihu fazu usred pranja od repa, a njegovo smanjenje može da prepolovi pranje. Ostavite prazno za podrazumevanu vrednost (0.98). Ovo je drugo podešavanje od Koeficijenta pametnog završavanja, koji upravlja zasebnom proverom ranog završetka.", + "label": "Koeficijent završavanja zaštite od gužvanja" + }, "anti_wrinkle_enabled": { "doc": "Prepoznajte kratke impulse male snage koje mašina za sušenje emituje nakon glavne faze grejanja i držite ih povezanim sa završenim ciklusom umesto da ih čitate kao nove cikluse.", "label": "Omogući detekciju zaštite od gužvanja" @@ -1182,7 +1210,7 @@ "label": "Maks. snaga zaštite od gužvanja" }, "auto_label_confidence": { - "doc": "Ako je rezultat meča na kraju ciklusa na ili iznad ovog, program se automatski označava bez ikakvog upita za potvrdu. Povećajte ga da zahteva veću sigurnost pre automatskog označavanja; spustite ga da biste više automatizovali. Radi u kombinaciji sa Learning Confidence ispod njega.", + "doc": "Ako je rezultat podudaranja na kraju ciklusa jednak ili veći od ovoga, program se automatski označava bez ikakvog upita za potvrdu. Povećajte ga da zahteva veću sigurnost pre automatskog označavanja; spustite ga da biste više automatizovali. Radi u kombinaciji sa Pouzdanošću učenja ispod njega.", "label": "Pouzdanost automatskog označavanja" }, "auto_maintenance": { @@ -1193,6 +1221,10 @@ "doc": "Ciklusi kraći od ovog se odbacuju kao ciklusi duhova (probni radovi, otvaranje vrata da se doda čarapa).", "label": "Min. trajanje ciklusa" }, + "curve_preroll_seconds": { + "doc": "Koliko sekundi očitavanja iz prekinutih pokušaja pokretanja WashData sme da prenese na početak krive ciklusa. Kriva ciklusa obično počinje od pokušaja pokretanja koji konačno uspe, pa kod mašine koja nekoliko puta proba da krene pre nego što se ustali - izbor programa, zaključavanje vrata, prvo punjenje vodom - prvi minuti stvarne aktivnosti nedostaju na svakoj krivoj. Podesite ovu vrednost malo iznad najdužeg takvog zaleta koji vaša mašina ima (300 s je za većinu više nego dovoljno). 0 isključuje ovu funkciju. Imajte u vidu da uključivanje pomera zabeleženi početak ranije, pa ciklusi zabeleženi pre i posle promene nose različita trajanja za isti program; naučeni proseci će odstupati dok stariji ciklusi ne isteknu.", + "label": "Uvodne sekunde krive" + }, "delay_confirm_seconds": { "doc": "Napajanje mora ostati u opsegu pripravnosti ovoliko dugo pre nego što se uređaj tretira kao da čeka da se pokrene, a ne da radi.", "label": "Sekunde potvrde" @@ -1230,11 +1262,11 @@ "label": "Broj ponavljanja kraja" }, "energy_price_entity": { - "doc": "Senzor sa trenutnom cenom električne energije po kVh (npr. dinamička tarifa). Ima prednost nad statičnom cenom ispod. Svaki ciklus zamrzava cenu na snazi kada se završi.", + "doc": "Senzor sa trenutnom cenom struje po kWh (npr. dinamična tarifa). Mora biti cena, a ne sopstveni entitet snage ili energije utičnice: ima prednost nad statičnom cenom ispod, pa bi brojilo kWh ovde obračunalo svaki ciklus po očitavanju brojila umesto po vašoj tarifi. Kada je Vremenski ponderisan trošak uključen, svaki ciklus se naplaćuje po ceni koja je važila u svakom trenutku njegovog rada; u suprotnom se na njega zamrzava cena koja je važila kada je završio.", "label": "Entiteta cene energije" }, "energy_price_static": { - "doc": "Fiksna cena po kVh koja se koristi za brojke troškova kada nije postavljen nijedan entitet cene uživo iznad.", + "doc": "Fiksna cena po kWh koja se koristi za brojke troškova kada iznad nije postavljen nijedan entitet cene uživo.", "label": "Statična cena energije (po kWh)" }, "energy_sensor": { @@ -1258,11 +1290,11 @@ "label": "Invertuj spoljni okidač (okidaj na ISKLJUČENO)" }, "learning_confidence": { - "doc": "Ako rezultat podudaranja pada između ovog i Auto-Label Confidence, obaveštenje o povratnim informacijama traži od vas da verifikujete identifikovani program. Ispod ovog rezultata meč je previše neizvestan da bi se pojavio. Mora se držati ispod pouzdanosti automatske oznake.", + "doc": "Ako rezultat podudaranja padne između ovog i Pouzdanosti automatskog označavanja, obaveštenje o povratnim informacijama traži od vas da verifikujete identifikovani program. Ispod ovog rezultata podudaranje je previše nesigurno da bi se prikazalo. Mora se držati ispod Pouzdanosti automatskog označavanja.", "label": "Pouzdanost učenja" }, "linked_device": { - "doc": "Opciono ugnezdite ovaj VashData uređaj u drugi uređaj (npr. pametni priključak) u registrator HA uređaja, prikazan kao „Povezan preko...“.", + "doc": "Opciono ugnezdite ovaj WashData uređaj u drugi uređaj (npr. pametni priključak) u registru HA uređaja, prikazan kao „Povezan preko ...“.", "label": "Grupiši pod uređaj" }, "min_off_gap": { @@ -1270,7 +1302,7 @@ "label": "Min. razmak isklopa" }, "min_power": { - "doc": "Apsolutna minimalna snaga smatra se aktivnom. Očitavanja ispod ovoga se tretiraju kao 0 V (pripravnost), filtrirajući fantomsko opterećenje pametnih utikača i LED dioda u stanju pripravnosti.", + "doc": "Apsolutna minimalna snaga koja se smatra aktivnom. Očitavanja ispod ovoga se tretiraju kao 0 W (pripravnost), filtrirajući fantomsko opterećenje pametnih utikača i LED dioda u stanju pripravnosti.", "label": "Minimalna snaga" }, "ml_training_enabled": { @@ -1322,11 +1354,11 @@ "label": "Servisi pri završetku" }, "notify_fire_events": { - "doc": "Takođe aktivirajte ha_vashdata_* događaje na početku/završetku ciklusa kako biste mogli da napravite sopstvene automatizacije.", + "doc": "Takođe aktivirajte ha_washdata_* događaje na početku/završetku ciklusa kako biste mogli da napravite sopstvene automatizacije.", "label": "Pokretanje HA događaja za obaveštenja" }, "notify_icon": { - "doc": "Opciona mdi ikona za obaveštenje (npr. mdi:mašina za pranje veša). Ostavite prazno za podrazumevanu platformu.", + "doc": "Mala ikona prikazana na obaveštenju (npr. mdi:washing-machine). Android je prikazuje u statusnoj traci; iOS je prikazuje umesto ikone aplikacije i zahteva mobilnu aplikaciju Home Assistant (companion app) 2026.8 ili noviju. Ostavite prazno za podrazumevanu ikonu platforme.", "label": "Ikona obaveštenja" }, "notify_live_chronometer": { @@ -1402,7 +1434,7 @@ "label": "Prag isključivanja napajanja" }, "power_sensor": { - "doc": "Entitet senzora koji izveštava o snazi u vatima za ovaj uređaj (npr. sensor.vasher_pover). Sve detekcije ciklusa se zasnivaju na ovom signalu.", + "doc": "Entitet senzora koji izveštava o snazi u vatima za ovaj uređaj (npr. sensor.washer_power). Sve detekcije ciklusa se zasnivaju na ovom signalu.", "label": "Senzor snage" }, "profile_duration_tolerance": { @@ -1426,7 +1458,7 @@ "label": "Prag podudaranja" }, "profile_unmatch_threshold": { - "doc": "Ako utakmica u sredini ciklusa uživo padne ispod ovog rezultata, privremena identifikacija se briše. Držite ga malo ispod praga podudaranja tako da kratko smanjenje sličnosti ne bi vratilo ekran na neuporedivo.", + "doc": "Ako podudaranje uživo usred ciklusa padne ispod ovog rezultata, privremena identifikacija se briše. Držite ga malo ispod Praga podudaranja kako kratak pad sličnosti ne bi vratio prikaz nazad na stanje bez podudaranja.", "label": "Prag nepodudaranja" }, "progress_reset_delay": { @@ -1437,10 +1469,6 @@ "doc": "Sekunde kada pumpa može da radi neprekidno pre nego što bude označena kao da je moguće zaglavljena (pokreće događaj zaglavljen-pump).", "label": "Trajanje zaglavljene pumpe" }, - "running_dead_zone": { - "doc": "Nakon što ciklus započne, padovi snage u ovom prozoru se ignorišu. Mašine za pranje veša se pune hladnom vodom (pada blizu 0 V pre zagrevanja) - bez ove zaštite ta faza punjenja izgleda kao završetak ciklusa. Ovo NE preskače podatke: puni trag snage se snima od T=0. Mehanizam za predloge meri stvarni obrazac pokretanja vaše mašine i automatski ga određuje.", - "label": "Mrtva zona rada" - }, "sampling_interval": { "doc": "Očekivano vreme između očitavanja senzora - koristi se za dimenzionisanje prozora za izglađivanje i pravilno pokretanje odbijanja. Svako ažuriranje senzora se snima bez obzira na ovu vrednost; samo kalibriše nizvodne proračune. Mehanizam za predloge meri stvarnu kadencu vašeg senzora iz prošlih ciklusa i automatski ga postavlja.", "label": "Interval uzorkovanja" @@ -1538,7 +1566,7 @@ }, "keep_min_score": { "label": "Min. rezultat podudaranja", - "doc": "Minimalni rezultat sličnosti koji kandidat mora imati da bi ostao u utrci podudaranja. Podrazumevana vrednost 0,1 je namerno benigna - prihvata čak i slabe kandidate i oslanja se na kasnijе faze za pronalazak najboljeg podudaranja. Povećajte za ranije odbacivanje neprobabilnih profila; smanjite za proširenje početnog skupa kandidata." + "doc": "Minimalni rezultat sličnosti koji kandidat mora imati da bi ostao u utrci podudaranja. Podrazumevana vrednost 0,1 je namerno benigna - prihvata čak i slabe kandidate i oslanja se na kasnije faze za pronalazak najboljeg podudaranja. Povećajte za ranije odbacivanje neprobabilnih profila; smanjite za proširenje početnog skupa kandidata." }, "dtw_blend": { "label": "Mešanje deformacije", @@ -1568,13 +1596,17 @@ "label": "Interval profila snage", "doc": "Veličina segmenta za atribut senzora power_profile po profilu (ravni niz prosečnih vata po slotu koji koriste spoljni planeri kao EMHASS i tibber_prices). Manji segmenti čuvaju kratke skokove snage oštrim; veći segmenti izglađuju oblik. Podrazumevano 15 min. Samo za čitanje; ne utiče na detekciju." }, + "notify_live_silent": { + "label": "Tiha ažuriranja uživo", + "doc": "Samo za iOS. Napredak uživo osvežavajte tiho: svako ažuriranje stiže bez zvuka i vibracije, sa nižim prioritetom koji štedi bateriju i koji iOS može nakratko da grupiše. Prvo ažuriranje u ciklusu (ono koje pokreće Live Activity), kao i obaveštenja o početku i završetku, i dalje se oglašavaju kao i obično. Isključite ovo da biste bili obavešteni pri svakom ažuriranju." + }, "notify_live_sticky": { "label": "Zadrži živo obaveštenje na dodir", "doc": "Samo za Android. Neka obaveštenje o napretku u živo bude trajno (zakačeno) kako ga dodir ne bi odbacio. Isključeno zadržava podrazumevano ponašanje gde dodir odbacuje obaveštenje." }, "notify_live_click_action": { - "label": "Cilj dodira živog obaveštenja", - "doc": "Samo za Android. Gde se otvara dodir na obaveštenje o napretku u živo (npr. /lovelace/laundry ili puni URL) umesto početne stranice aplikacije. Ostavite praznim za podrazumevano ponašanje." + "label": "Cilj dodira obaveštenja", + "doc": "Kuda vodi dodir obaveštenja sa ovog uređaja, na sistemima Android i iOS. Prazno polje otvara WashData panel na ovom uređaju. Unesite putanju kontrolne table (npr. /lovelace/laundry) ili pun URL da vodi negde drugde, ili „none“ ako dodir ne treba ništa da otvara." }, "door_opens_at_end": { "label": "Vrata se automatski otvaraju na kraju", @@ -1587,6 +1619,10 @@ "profile_evidence_sources": { "label": "Ciklusi koji grade program", "doc": "Koji se ciklusi koriste za izgradnju krive snage svakog programa i za podudaranje završenog ciklusa s njom. Kada odznačite neku vrstu, ona više ne utiče na vaše programe, ali se ništa ne briše - ciklusi ostaju na spisku Ciklusi i mogu se i dalje označiti ili ukloniti. Korisno ako ne verujete uvezenim podacima. Na statistiku to ne utiče: uvek se računaju samo ciklusi koje je ovaj uređaj zaista izvršio. Odznačavanje svega se ignoriše jer program bez ikakvih ciklusa nikada ne bi mogao da se podudari." + }, + "energy_price_dynamic": { + "label": "Vremenski ponderisan trošak", + "doc": "Naplaćuje svaki ciklus po ceni koja je važila u svakom trenutku njegovog rada, umesto po jednoj ceni koja je važila kada je završio. Odnosi se samo na entitet cene (statična cena ne može da se menja). Ne zahteva dodatno podešavanje: cena se prati uživo dok ciklus traje, a Obrada istorije u Dijagnostici ponovo obračunava starije cikluse iz recordera. Isključeno = klasično ponašanje, jedna cena zamrznuta na kraju." } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Deljenje…", "deleting": "Brisanje…", "imported": "Uvezeno", - "preparing": "Priprema…" + "preparing": "Priprema…", + "unlabelling": "Uklanjanje oznake…" }, "suggestion": { "both_agree": "WashData preporučuje", @@ -1745,10 +1782,6 @@ "higher": "Potvrđuje samo visoko pouzdana podudaranja; više ciklusa ostaje neoznačeno", "lower": "Podudara više ciklusa; neki mogu biti blago pogrešni" }, - "running_dead_zone": { - "higher": "Zanemaruje kratke skokove snage tokom mirovanja – manje osetljiv na šum", - "lower": "Reaguje na kraće impulse snage – otkriva brzu prolaznu aktivnost" - }, "start_threshold_w": { "higher": "Sprečava lažne startove od kratkih skokova snage", "lower": "Otkriva programe s niskom snagom; može reagovati na šum" @@ -1773,6 +1806,8 @@ "end_repeat_count": "{false}/{total} čistih ciklusa ({pct}%) imalo je unutrašnju pauzu dužu od 60s koja se nastavila; zahtevaj {val} potvrdu(e) kraja.", "learning_confidence": "Pouzdanost p05 za {cycles} korisnički označenih ciklusa ({p05}); ispod ove vrednosti zahtevaj proveru.", "match_interval": "Na osnovu uočene učestalosti ažuriranja (medijana={median}s) * 10.", + "match_interval_capped": "Ograničeno tako da {persistence} uzastopnih podudaranja stane u {pct}% najkraćeg programa ({shortest}s); sama učestalost ažuriranja (medijana={median}s) predložila bi duži interval.", + "match_interval_floored": "Već bi i sam najkraći program ({shortest}s) ovu vrednost ograničio na {cap}s, ispod minimuma od {minimum}s, pa se zadržava na minimumu: niz od {persistence} uzastopnih podudaranja traje {budget}s, što je više od {pct}% tog programa. Podudaranje se pokreće samo kada stigne novo očitanje (medijana={median}s), pa minimum ne donosi dodatno opterećenje.", "max_duration_ratio": "Na osnovu trajanja označenih ciklusa (p95={p95}).", "min_active": "Na osnovu najmanje radne snage ({min}W) uočene u poslednjem ciklusu.", "min_duration_ratio": "Zadržano što nižim kako bi se program prepoznao rano u ciklusu; pragovi pouzdanosti i dvosmislenosti sprečavaju prevremene potvrde.", @@ -1891,6 +1926,7 @@ "recording_stopped": "Snimanje zaustavljeno", "review_saved": "Pregled sačuvan", "settings_conflicts": "Ispravite istaknute konflikte podešavanja pre čuvanja.", + "settings_no_changes": "Nema promena za čuvanje", "settings_reverted": "Podešavanja vraćena; integracija se ponovo učitava", "settings_saved": "Podešavanja sačuvana; integracija se ponovo učitava", "suggestions_applied": "Prijedlozi primenjeni; integracija se ponovo učitava", @@ -1949,7 +1985,11 @@ "export_selective_done": "Izvoz preuzet", "import_selective_done": "Uvezeno {profiles} profil(a) i {cycles} ciklus(a)", "hist_csv_required": "Prvo učitajte CSV datoteku ili nalepite njen sadržaj", - "file_read_failed": "Nije bilo moguće pročitati tu datoteku" + "file_read_failed": "Nije bilo moguće pročitati tu datoteku", + "unlabel_done": "Uklonjena oznaka sa {count} ciklus(a)", + "unlabel_failed": "Uklanjanje oznake nije uspelo: {error}", + "odometer_saved": "Ukupan broj ciklusa sačuvan", + "odometer_save_failed": "Nije moguće sačuvati ukupan broj: {error}" }, "trend": { "down": "Opadajući trend", @@ -1965,6 +2005,7 @@ "pg_desc": { "dishwasher_end_spike_quiet_release": "Mašina za pranje sudova: sekunde tišine nakon očekivanog trajanja pre nego što se otpusti čekanje na završno ispumpavanje vode", "smart_termination_duration_ratio": "Deo očekivanog trajanja podudarenog programa koji ciklus mora da dostigne pre nego što Pametno završavanje sme da ga okonča ranije; smanjite ga za mašine zavisne od količine veša ili temperature", + "anti_crease_finalize_ratio": "Deo očekivanog trajanja podudarenog programa koji ciklus mora da dostigne pre nego što završni rep prevrtanja zaštite od gužvanja sme da se okonča; smanjite ga kod mašina za sušenje sa dugim repom prevrtanja, mašine za pranje veša ostavite na podrazumevanoj vrednosti", "completion_min_seconds": "Najkraće pokretanje koje se računa kao pravi ciklus", "end_repeat_count": "Niska očitavanja zaredom pre završetka", "interrupted_min_seconds": "Kratki ciklusi označeni kao prekinuti", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Faza 3: rastojanje polu-zasićenja DDTW; manje = osetljivije na oblik (podrazumevano 30)", "dtw_refine_top_n": "Faza 3: kandidati koje DTW ponovo ocenjuje; povećajte na 7-9 ako ispravni profil zauzme 4.-5. mesto (podrazumevano 5)", "duration_scale": "Faza 4: odnos logaritma gde se slaganje trajanja prepolovi; manje = stroža kazna (podrazumevano 0,175)", - "energy_scale": "Faza 4: odnos logaritma gde se slaganje energije prepolovi; manje = stroža kazna (podrazumevano 0,25)" + "energy_scale": "Faza 4: odnos logaritma gde se slaganje energije prepolovi; manje = stroža kazna (podrazumevano 0,25)", + "curve_preroll_seconds": "Sekunde očitavanja iz prekinutih pokušaja pokretanja prenete na početak krive ciklusa; 0 = isključeno" }, "col": { "profile_tip": "Naziv podudarnog programa. Bez oznake znači da se na kraju ciklusa nijedan profil nije podudarao.", @@ -1998,10 +2040,11 @@ "date_tip": "Datum i vreme početka ciklusa.", "duration_tip": "Ukupno vreme rada ciklusa od početka do kraja.", "energy_tip": "Ukupna potrošena energija (kWh). Izračunato integracijom snage kroz vreme.", - "cost_tip": "Trošak energije za ovaj ciklus, zamrznut po završetku prema tada važećoj ceni (energija x cena po kWh). Postavite cenu u Podešavanjima da biste ga popunili.", + "cost_tip": "Trošak energije za ovaj ciklus, zamrznut po završetku. Kada je Vremenski ponderisan trošak uključen, kriva snage se naplaćuje po ceni koja važi u svakom trenutku; u suprotnom energija x jedna cena koja važi na kraju. Postavite cenu u Podešavanjima da se popuni.", "confidence_tip": "Pouzdanost podudaranja profila (0-100%). Koliko se tačno kriva snage ciklusa podudarala sa identifikovanim programom.", "health_tip": "Zdravlje ciklusa ML (više = bolje). Kliknite ciklus da biste ga pregledali i ocenili.", - "flags_tip": "Oznake zajednice i pokazatelji kvaliteta podataka" + "flags_tip": "Oznake zajednice i pokazatelji kvaliteta podataka", + "cost_dynamic_tip": "Vremenski ponderisano: naplaćeno po ceni koja je važila tokom ciklusa (efektivno {price} {cur}/kWh)." }, "stat": { "avg": "pros.", @@ -2028,9 +2071,9 @@ "finished": "Ciklus je dostigao završno stanje i završio." }, "store": { + "search_ph2": "Pretraži po marki ili modelu…", "your_model_tip": "Ovo je uređaj koji ste naveli u Podešavanjima", "your_model": "Vaš", - "search_brand_ph": "Pretraži po marki…", "programs_count": "Programi: {n}", "browse": "Pregledaj", "device": "Uređaj", @@ -2076,7 +2119,8 @@ "golden": "Ponovna obrada: popunjavanje referentnih", "suggestions": "Ponovna obrada: predlozi", "ml_training": "Ponovna obrada: ML obuka", - "health": "Ponovna obrada: zdravlje ciklusa" + "health": "Ponovna obrada: zdravlje ciklusa", + "costs": "Ponovna obrada: troškovi energije" }, "pg_sweep": { "optimize": "Optimizacija: {param}" diff --git a/custom_components/ha_washdata/translations/panel/sv.json b/custom_components/ha_washdata/translations/panel/sv.json index dc6381df..d082eeb6 100644 --- a/custom_components/ha_washdata/translations/panel/sv.json +++ b/custom_components/ha_washdata/translations/panel/sv.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Mot slutet blir det här programmet tyst i cirka {mins} min och drar sedan omkring {watts} W i {secs} s innan det är klart. Sett i {seen} av {measured} uppmätta cykler: apparaten gör det inte vid varje körning.", + "quiet_tail": "~{mins} min tyst slutfas", "artifact_tip": "{n} anomalier upptäckta (t.ex. dörren öppnades mitt i cykeln) – öppna för att se dem i diagrammet", "auto": "(automatiskt identifierat)", "built_in_tag": "inbyggd", @@ -63,7 +65,7 @@ "needs_review": "Behöver granskning", "overrun": "Pågick längre än vanligt", "poor_fit": "dålig kvalitet", - "poor_fit_tip": "Inkonsekvent matchhistorik – överväg att bygga om den här profilen", + "poor_fit_tip": "Inkonsekvent matchningshistorik: överväg att bygga om den här profilen", "restart_gap_tip": "{n} HA-omstartslucka: effektspåret har ett hål", "reviewed": "Granskad", "steady": "→ stabil", @@ -77,11 +79,14 @@ "approved": "Godkänd", "awaiting": "Väntar på godkännande", "imported_tip": "Importerad från gemenskapsbutiken. Används endast för matchning, räknas inte in i statistiken.", + "unmatchable": "kan inte matchas", "not_importable": "ej här", "exists": "finns", - "backfilled_tip": "Detekterad i importerad effekthistorik. Påverkar bara programmatchningen, räknas inte in i statistiken." + "backfilled_tip": "Detekterad i importerad effekthistorik. Påverkar bara programmatchningen, räknas inte in i statistiken.", + "armed": "(gäller nästa cykel)" }, "btn": { + "reset_zoom": "Återställ zoom", "set_brand_model": "Ange märke och modell", "refresh_catalog": "Uppdatera katalog", "add_device": "+ Lägg till enhet", @@ -215,6 +220,8 @@ "trim": "Beskär", "trim_split": "Beskär / Dela", "undo": "Ångra", + "unlabel_selected": "Ta bort märkning ({n})", + "unlabel_selected_tip": "Tar bort profilmärkningen från de valda cyklerna. De ligger kvar i din historik och kan märkas igen senare - de påverkar bara inte längre den här profilen.", "use": "Använd", "wipe_all": "Rensa all data", "delete_selected": "Radera valda ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "Använd bästa", "pg_apply_to_settings": "Spara till inställningar", "pg_apply_to_settings_tip": "Kopiera värdena du redigerade här till den här enhetens aktiva inställningar", - "search": "Sök", "import": "Importera", "disconnect": "Koppla från", "connect_github": "Anslut GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Läs från Home Assistant", "hist_scan": "Sök efter cykler", "hist_import_n": "Importera {n} cykler", - "hist_goto_cycles": "Visa mig cyklerna" + "hist_goto_cycles": "Visa mig cyklerna", + "save_total_cycles": "Spara total" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "Måste vara över avstängningströskel ({pot} W)", "threshold": "Måste vara under stopptröskel ({stop} W) för att ha effekt" }, + "price_entity": { + "power_sensor": "Det här är apparatens Effektsensor, inte ett pris. En prisentitet har företräde framför det statiska priset nedan, så cykler skulle debiteras till watt per kWh - töm det här fältet.", + "energy_sensor": "Det här är apparatens Energimätare, inte ett pris. En prisentitet har företräde framför det statiska priset nedan, så cykler skulle debiteras till mätarställningen per kWh - töm det här fältet.", + "device_class": "Den här sensorn mäter {dc}, inte ett pris per kWh. En prisentitet har företräde framför det statiska priset nedan - töm fältet eller peka det på en tariffsensor.", + "unit": "Den här sensorn visar värden i {unit}, vilket inte är ett pris per kWh. En prisentitet har företräde framför det statiska priset nedan - töm fältet eller peka det på en tariffsensor." + }, "pump_stuck": { "duration": "Måste vara kortare än timeout utan uppdatering ({to} s) så att fastnar-larmet utlöses innan vakthunden stoppar cykeln", "timeout": "Måste överskrida pumpens fastnar-varaktighet ({ps} s) så att larmet utlöses innan cykeln tvångsstoppas" @@ -349,7 +362,7 @@ "pg_settings_source": "Inställningarnas källa", "phase_catalog": "Faskatalog", "playground": "Testyta", - "process_history": "Processhistorik", + "process_history": "Bearbeta historik", "settings_history": "Inställningshistorik", "status": "Status", "status_graph": "Statusdiagram", @@ -360,12 +373,12 @@ "wipe_history": "Rensa historik", "recommendations": "Rekommendationer ({n})", "pg_detection_params": "Detekteringsinställningar", - "pg_timeline": "Händelsetidslinje", "pg_outcome": "Simuleringsresultat", "pg_across_cycles": "Över dina cykler", "community_store": "Gemenskapsbutik", "online_account": "Gemenskapsbutik och onlinefunktioner", - "import_power_history": "Importera effekthistorik" + "import_power_history": "Importera effekthistorik", + "service_status": "Servicestatus" }, "health": { "fair": "Acceptabel profilkvalitet", @@ -373,6 +386,10 @@ "poor": "⚠ Dålig profilkvalitet" }, "lbl": { + "envelope_position_tip": "Position på det matchade programmets egen registrerade kurva, {pct} %. Mäts genom att den här cykeln passas in mot profilen i stället för att tiden räknas, så den stämmer även när en körning är längre eller kortare än vanligt. Den uppdateras medan apparaten är tyst och kan därför släpa efter under en aktiv fas.", + "envelope_position": "kurva {pct} %", + "aria_playground_chart3": "Interaktiv effektgraf för cykeln: hovra för att avläsa tid/effekt, skrolla eller nyp för att zooma, dra för att panorera", + "device_unresolved": "Otillgänglig enhet ({id})", "drag_to_resize": "Dra för att ändra storlek", "actions": "Åtgärder", "activity": "Aktivitet", @@ -513,7 +530,7 @@ "profile_index": "profilindex →", "profile_name": "Profilnamn", "program": "Program", - "program_tip": "Åsidosätt vilken profil som matchas mot det aktuella cykeln. Autodetektering låter integrationen välja den bästa matchningen automatiskt. Fäst ett specifikt program för att tvinga matchning när autodetekteringen är fel eller du vet vad som körs.", + "program_tip": "Åsidosätt vilken profil som matchas mot det aktuella cykeln. Autodetektering låter integrationen välja den bästa matchningen automatiskt. Fäst ett specifikt program för att tvinga matchning när autodetekteringen är fel eller du vet vad som körs. Välj ett innan du startar apparaten, så tillämpas det så snart nästa cykel börjar.", "progress": "Förlopp", "raw_socket": "Råsocket", "read_only": "Skrivskyddad", @@ -548,6 +565,7 @@ "stage4": "Steg 4 – överensstämmelse", "status": "Status", "tail_trim": "Beskär slut (s)", + "tap_to_dismiss": "tryck på diagrammet för att stänga", "timer_auto_pause": "Automatisk paus", "timer_min": "min", "timer_msg_placeholder": "Meddelande (valfritt, {device}/{program}/{minutes})", @@ -584,9 +602,7 @@ "pg_time_left_model": "Tid kvar (modell)", "pg_time_left_model_tip": "Modellberäknad återstående tid (fasberäknare + ML-blandning), inte en statisk nedräkning", "phase": "Fas", - "pg_mode_simulate": "Simulera", "pg_mode_history": "Testa på historik", - "pg_mode_sweep": "Svep", "pg_ev_detected": "Upptäckt", "pg_ev_match": "Matchning bekräftad", "pg_ev_match_changed": "Matchning ändrad", @@ -616,14 +632,11 @@ "pg_sweep_param": "Parameter", "pg_objective": "Mål", "pg_steps": "Steg", - "pg_sweep_2d": "2D-värmekarta", - "pg_sweep_param_y": "2:a parametern", "pg_obj_match": "Matchningsnoggrannhet", "pg_obj_endtiming": "Sluttidsnoggrannhet", "pg_obj_falseend": "Falskt-slut-andel", "pg_obj_overrun": "Varaktighetsavvikelse", "pg_obj_ambiguity": "Tvetydighetsandel", - "pg_best_combo": "bästa kombinationen", "pg_mode_optimize": "Optimera", "pg_group_matching": "Programmatchning", "pg_zoom_hint": "dubbelklicka för att återställa zoomen", @@ -706,7 +719,12 @@ "task_history_import_apply": "Importerar cykler", "evidence_real_cycles": "Cykler den här maskinen har kört", "evidence_reference_cycles": "Nedladdade från gemenskapsbutiken", - "evidence_backfill_cycles": "Hittade i importerad effekthistorik" + "evidence_backfill_cycles": "Hittade i importerad effekthistorik", + "zoom_hint_touch": "nyp för att zooma", + "zoom_hint_touch_zoomed": "nyp för att zooma · dubbeltryck för att återställa", + "cycles_since_service": "{since} / {total} cykler", + "total_cycles_run": "cykler totalt", + "correct_total_cycles": "Korrigera totalen" }, "log": { "all_levels": "Alla nivåer", @@ -756,7 +774,7 @@ "merge_cycles": "Slå samman {n} cykler", "new_group": "Ny profilgrupp", "new_phase": "Ny fas", - "process_history_msg": "Kör matchning igen, uppdatera förslag, träna om ML (om aktiverat) och beräkna om cyklernas hälsa för alla lagrade cykler. Detta kan ta ett tag.", + "process_history_msg": "Kör matchningen igen, uppdatera förslagen, träna om ML (om aktiverat), räkna om cyklernas kostnad mot registrerade energipriser och beräkna om cyklernas hälsa för alla lagrade cykler. Detta kan ta ett tag.", "process_history_ok": "Bearbeta", "process_history_title": "Bearbeta historik", "process_recording": "Bearbeta inspelning", @@ -789,6 +807,7 @@ "history_import": "Importera effekthistorik" }, "msg": { + "pg_canvas_empty3": "Välj en cykel ovan och tryck på Kör för att simulera den. Hovra sedan för att avläsa värden, skrolla eller nyp för att zooma och dra för att panorera.", "tail_trim_hint": "Ta bort så här många sekunder från slutet", "store_sibling_hint": "Finns inget delat för exakt din modell? En närbesläktad modell från samma märke är oftast en bra utgångspunkt.", "store_declare_appliance": "Berätta för WashData vilken apparat du har, så visar den här fliken de färdiga inställningar andra har delat för den. Du kan också skriva ett märke ovanför för att se dig omkring.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Drog över det normala effektbandet i ~{n} s.", "auto_label_intro": "Tilldela profiler till omärkta cykler vars matchningskonfidens överstiger tröskeln.", "automations_intro": "WashData utlöser {start} / {end}-händelser och tillhandahåller entiteter – aviseringar och åtgärder byggs bäst som vanliga Home Assistant-automatiseringar. Automatiseringar som använder den här enheten visas nedan.", - "cleanup_intro": "Alla märkta cykler är överlagda. Kryssa i avvikare och ta bort dem för att rensa profilen.", + "cleanup_intro": "Alla märkta cykler är överlagrade. Kryssa i avvikare och ta sedan bort deras märkning (de ligger kvar i historiken men påverkar inte längre den här profilen) eller radera dem helt.", "clear_debug_hint": "Ta bort lagrad felsökningsdata för att frigöra utrymme.", "collecting_data": "Samlar in data. Cykler som återstår innan finjustering kan starta: {need} ({current}/{min}).", "compare_overlay_profiles": "Lägg över profiler (svag)", @@ -901,7 +920,7 @@ "preferences_admin": "Inställningar, panel och åtkomstkontroll", "preferences_adv": "Inställningar", "prefs_personal": "Dessa gäller endast ditt Home Assistant-konto.", - "process_history_hint": "Kör matchning igen på alla lagrade cykler, uppdatera justeringsförslag, träna om ML-modellerna (om aktiverat) och beräkna om cyklernas hälsa. Kör detta efter en omgång granskningar.", + "process_history_hint": "Kör matchningen igen på alla lagrade cykler, uppdatera justeringsförslagen, träna om ML-modellerna (om aktiverat), räkna om cyklernas kostnad mot dina registrerade energipriser och beräkna om cyklernas hälsa. Kör detta efter en omgång granskningar.", "profile_deleted": "Profil borttagen", "profile_poor_health_detail": "Cykler som tilldelats den här profilen har inkonsekventa former eller låg konfidens. Överväg att bygga om kuvertet eller granska märkta cykler.", "profiles_intro": "Klicka på en profil för statistik, faser och rensning. Gruppera nästan identiska program (samma form/varaktighet, olika temperatur eller centrifugering) så att matchning tillförlitligt väljer mellan dem.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' har ett lågt passningsvärde - dess senaste cykler varierar mycket eller matchar svagt. Granska dess cykler eller spela in profilen på nytt så att matchning och tidsuppskattningar förblir korrekta.", "advisory_shape_drift": "'{name}' har drivit iväg avsevärt från sin ursprungliga effektkurva. Apparaten kan ha förändrat sitt beteende över tid (t.ex. kalkavlagringar, slitage). Överväg att spela in profilen på nytt med aktuella cykler.", "advisory_shape_drift_corr": "'{name}' har drivit iväg avsevärt från sin ursprungliga effektkurva (korrelation {corr}). Apparaten kan ha förändrat sitt beteende över tid (t.ex. kalkavlagringar, slitage). Överväg att spela in profilen på nytt med aktuella cykler.", + "advisory_unmatchable": "'{name}' kan aldrig matchas: det finns ingen cykel med effektdata bakom den, så WashData har inget att jämföra en pågående cykel med. Märk en cykel av det här programmet (eller spela in en) för att få med det i matchningen igen.", + "advisory_unmatchable_no_duration": "'{name}' kan aldrig matchas: cykeln bakom den har effektdata men ingen användbar längd, så WashData har ingen längd att mäta en matchning mot. Spela in det här programmet igen, eller ange dess förväntade längd, för att få med det i matchningen igen.", "pg_no_alerts": "Inga problem upptäcktes i den här körningen.", "pg_override_active": "Använder dina redigerade inställningar mot de nuvarande", - "pg_history_intro": "Spela upp dina senaste cykler genom den riktiga detekteringen och matchningen. Redigera en detekteringsinställning i Simulera först för att se en före/efter-jämförelse här.", "pg_history_empty": "Tryck på Kör för att spela upp dina cykler igen.", "pg_history_summary": "{detected}/{total} upptäckta · {correct} matchade korrekt", "pg_sweep_no_metric": "Inte tillräckligt med data för att poängsätta det här målet.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Sökningen misslyckades.", "hist_scan_expired": "Den sökningen är inte längre tillgänglig. Sök igen.", "hist_import_failed": "Importen misslyckades.", - "imported_history_readonly": "Detekterad i importerad effekthistorik. Den påverkar programmatchningen men räknas inte in i din statistik och kan inte beskäras eller delas. Märk den för att namnge programmet." + "imported_history_readonly": "Detekterad i importerad effekthistorik. Den påverkar programmatchningen men räknas inte in i din statistik och kan inte beskäras eller delas. Märk den för att namnge programmet.", + "odometer_intro": "Den här totalsumman kan bara öka. Att ta bort en cykelpost ändrar den inte, och den fortsätter räkna även utöver gränsen för sparad historik, så att servicepåminnelserna fortsätter stämma.", + "odometer_correct_hint": "Ange detta om WashData har registrerat en körning som aldrig ägde rum, eller om apparaten redan hade kört cykler innan WashData installerades.", + "no_reminders_set": "Inga servicepåminnelser är inställda än.", + "toast_program_armed": "Program valt för nästa cykel: {program}" }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Diskmaskin: tysta sekunder efter förväntad varaktighet innan väntan på tömning vid cykelslut frisläpps", @@ -1082,7 +1106,7 @@ "energy_weight": "Steg 4: hur starkt energiöverensstämmelse påverkar slutpoängen (standard 0,22)", "profile_match_min_duration_ratio": "Steg 1: lägsta cykellängd som andel av profilduration; standard 0,1 (10%)", "profile_match_max_duration_ratio": "Steg 1: högsta cykellängd som andel av profilduration; standard 1,5 (150%)", - "keep_min_score": "Steg 2: lägsta poäng för att vara kvar i matchen; standard 0,1 tillåter svaga kandidater, senare steg väljer bäst", + "keep_min_score": "Steg 2: lägsta poäng för att gå vidare; standard 0,1 släpper igenom svaga matchningar, senare steg väljer den bästa", "dtw_blend": "Steg 3: 0 = bara kärnpoäng, 1 = bara DTW-poäng, 0,5 = lika blandning (standard)", "dtw_ensemble_w": "Steg 3 (ensemble-läge): vikt på skalad L1 kontra derivativ DTW; standard 0,7 gynnar nivåmedvetenhet", "dtw_ddtw_scale": "Steg 3: DDTW-halvmättnadsavstånd; lägre = mer formkänslig (standard 30)", @@ -1093,7 +1117,9 @@ "anti_wrinkle_max_power": "En puls över detta avslutar skrynkelskyddet och öppnar en ny cykel", "anti_wrinkle_enabled": "Fånga upp tumlingspulserna efter huvudfasen i stället för att läsa dem som nya cykler", "anti_wrinkle_max_duration": "En puls längre än detta avslutar skrynkelskyddet och öppnar en ny cykel", - "anti_wrinkle_exit_power": "Effekten måste sjunka under detta mellan pulserna för att skrynkelskyddet ska förbli aktivt" + "anti_wrinkle_exit_power": "Effekten måste sjunka under detta mellan pulserna för att skrynkelskyddet ska förbli aktivt", + "anti_crease_finalize_ratio": "Andel av det matchade programmets förväntade varaktighet som en cykel måste nå innan skrynkelskyddssvansen får avslutas; sänk den på torktumlare med lång svans, lämna tvättmaskiner på standardvärdet", + "curve_preroll_seconds": "Sekunders mätvärden från avbrutna startförsök som tas med främst i en cykels kurva; 0 = av" }, "phase_desc": { "anti_crease": "Enstaka korta tumlar efter färdigställandet för att förhindra skrynklor i tvätten.", @@ -1191,6 +1217,10 @@ "label": "Frisläpp efter tyst torkfas", "doc": "När cykeln passerar sin förväntade varaktighet: hur länge diskmaskinen måste förbli tyst (under Stopptröskel) innan WashData slutar vänta på en sista tömning och avslutar cykeln. Höj värdet om din maskin har en lång tyst torkfas före en sen sista tömning som missas - ett bredare fönster låter den inlärda varaktigheten följa säsongsvariationer (kallare inloppsvatten = längre cykler) istället för att låsa sig vid det gamla medelvärdet. Det förkortar bara väntetiden i förhållande till den interna gränsen på 30 minuter för slutspiken, aldrig förlänger den." }, + "anti_crease_finalize_ratio": { + "label": "Kvot för skrynkelskyddsavslut", + "doc": "Hur långt in i det matchade programmets förväntade varaktighet en cykel måste vara innan WashData får avgöra att den lågeffektstumling den ser är skrynkelskyddssvansen och avsluta cykeln där. Den förväntade varaktigheten är programmets genomsnitt, så på en torktumlare vars körtid med fuktsensor följer tvättmängden når de flesta körningar aldrig 98% av den, och svansen blir kvar till reserv-timeouten i stället - ofta 15 till 40 minuter efter att maskinen faktiskt stannade. Sänk detta värde (t.ex. 0,75) på en sådan torktumlare. Lämna tvättmaskiner på standardvärdet: en tvättmaskin ligger under Maks. skrynkelskyddseffekt under största delen av sin cykel, så den här andelen är det enda som skiljer en tyst fas mitt i tvätten från svansen, och en sänkning kan halvera en tvätt. Lämna tomt för standardvärdet (0,98). Detta är en annan inställning än Kvot för smart avslutning, som styr en separat kontroll för tidigt avslut." + }, "anti_wrinkle_enabled": { "doc": "Känn igen de korta torktumlarpulser med låg effekt en torktumlare avger efter huvuduppvärmningsfasen och håll dem kopplade till den färdiga cykeln istället för att läsa dem som nya cykler.", "label": "Aktivera skrynkelskyddsdetektering" @@ -1223,6 +1253,10 @@ "doc": "Cykler som är kortare än detta kasseras som spökcykler (testkörningar, öppna dörren för att lägga till en strumpa).", "label": "Min. cykelvaraktighet" }, + "curve_preroll_seconds": { + "label": "Kurvupptakt", + "doc": "Hur många sekunders mätvärden från avbrutna startförsök WashData får ta med främst i en cykels kurva. En cykels kurva börjar normalt vid det startförsök som till slut lyckas, så på en maskin som trevar ett par gånger innan den kommer i gång på riktigt - programval, dörrlås, första vattenintag - saknas de första minuterna av verklig aktivitet i varje kurva. Ställ in det här lite högre än den längsta sådana upptakt din maskin har (300 s räcker gott för de flesta). 0 stänger av det. Tänk på att när det slås på flyttas den registrerade starten tidigare, så cykler från före och efter ändringen bär olika varaktigheter för samma program; de inlärda medelvärdena kommer att glida tills de äldre cyklerna fallit ur." + }, "delay_confirm_seconds": { "doc": "Strömmen måste stanna i standby-bandet så länge innan apparaten behandlas som att den väntar på att starta snarare än att den är igång.", "label": "Bekräftelsesekunder" @@ -1260,7 +1294,7 @@ "label": "Antal slutrepetitioner" }, "energy_price_entity": { - "doc": "Sensor med aktuellt elpris per kWh (t.ex. en dynamisk tariff). Har företräde framför det statiska priset nedan. Varje cykel fryser det pris som gäller när den är klar.", + "doc": "Sensor med aktuellt elpris per kWh (t.ex. en dynamisk tariff). Måste vara ett pris, inte uttagets egen effekt- eller energientitet: det har företräde framför det statiska priset nedan, så en kWh-räknare här debiterar varje cykel till mätarställningen i stället för till din tariff. Med Tidsviktad kostnad på debiteras varje cykel till det pris som gällde i varje ögonblick den kördes; annars fryses det pris som gällde när den var klar fast på den.", "label": "Energiprisentitet" }, "energy_price_static": { @@ -1288,7 +1322,7 @@ "label": "Invertera extern utlösare (utlös vid AV)" }, "learning_confidence": { - "doc": "Om matchresultatet faller mellan detta och Auto-Label Confidence, uppmanas du i ett feedbackmeddelande att verifiera det identifierade programmet. Under denna poäng är matchen för osäker för att ytan. Måste hållas under Auto-Label Confidence.", + "doc": "Om matchningspoängen hamnar mellan detta värde och Automärkningskonfidens uppmanas du i ett feedbackmeddelande att verifiera det identifierade programmet. Under denna poäng är matchningen för osäker för att visas. Måste hållas under Automärkningskonfidens.", "label": "Inlärningskonfidens" }, "linked_device": { @@ -1356,7 +1390,7 @@ "label": "Avfyra HA-händelser för meddelanden" }, "notify_icon": { - "doc": "Valfri mdi-ikon för meddelandet (t.ex. mdi:tvättmaskin). Lämna tomt för plattformens standard.", + "doc": "Den lilla ikonen som visas på meddelandet (t.ex. mdi:washing-machine). Android ritar den i statusfältet; iOS visar den i stället för appens ikon och kräver companion-appen 2026.8 eller senare. Lämna tomt för plattformens standard.", "label": "Meddelandeikon" }, "notify_live_chronometer": { @@ -1456,7 +1490,7 @@ "label": "Matchningströskel" }, "profile_unmatch_threshold": { - "doc": "Om en livematch i mitten av cykeln sjunker under denna poäng, rensas den preliminära identifieringen. Håll den lite under matchtröskeln så att en kort dopp i likhet inte vänder tillbaka skärmen till oöverträffad.", + "doc": "Om en pågående matchning mitt i cykeln sjunker under denna poäng rensas den preliminära identifieringen. Håll den lite under Matchningströskeln så att ett kort fall i likhet inte får visningen att gå tillbaka till Ingen matchning.", "label": "Omatchningströskel" }, "progress_reset_delay": { @@ -1467,10 +1501,6 @@ "doc": "Sekunder kan en pump köras kontinuerligt innan den flaggas som eventuellt fast (utlöser händelsen med fast pump).", "label": "Pumpens fastnadsvaraktighet" }, - "running_dead_zone": { - "doc": "Efter att en cykel har startat ignoreras effektfall inom detta fönster. Tvättmaskiner fylls med kallt vatten (sjunker nära 0 W före uppvärmning) - utan detta skydd ser fyllningsfasen ut som ett cykelslut. Detta hoppar INTE över data: spåret för full effekt registreras från T=0. Förslagsmotorn mäter din maskins faktiska startmönster och dimensionerar detta automatiskt.", - "label": "Löpande dödzon" - }, "sampling_interval": { "doc": "Förväntad tid mellan sensoravläsningarna - används för att dimensionera utjämningsfönstret och starta avstudsningen korrekt. Varje sensoruppdatering registreras oavsett detta värde; den kalibrerar bara nedströmsberäkningarna. Förslagsmotorn mäter din sensors faktiska kadens från tidigare cykler och ställer in denna automatiskt.", "label": "Samplingsintervall" @@ -1598,13 +1628,17 @@ "label": "Effektprofil-intervall", "doc": "Storlek på bucket för effektprofilsensorns attribut power_profile (det platta arrayen med genomsnittliga wattvärden per slot som används av externa planerare som EMHASS och tibber_prices). Mindre buckets håller korta effekttoppar skarpa; större buckets jämnar ut kurvan. Standard 15 min. Endast för visning; påverkar inte detektering." }, + "notify_live_silent": { + "label": "Tysta liveuppdateringar", + "doc": "Endast iOS. Uppdatera liveförloppet ljudlöst: varje uppdatering kommer utan ljud eller vibration, med en lägre, batterisnål prioritet som iOS kan gruppera en kort stund. Den första uppdateringen i en cykel (den som startar liveaktiviteten) samt start- och slutaviseringarna hörs fortfarande som vanligt. Stäng av detta om du vill bli aviserad vid varje uppdatering." + }, "notify_live_sticky": { "label": "Behåll live-avisering vid tryck", "doc": "Endast Android. Gör live-förloppsaviseringen kvarstående (sticky) så att ett tryck inte stänger den. Av: standardbeteende där ett tryck stänger aviseringen." }, "notify_live_click_action": { - "label": "Tryckmål för live-avisering", - "doc": "Endast Android. Vart ett tryck på live-förloppsaviseringen öppnar (t.ex. /lovelace/laundry eller en fullständig URL) i stället för appens startsida. Lämna tomt för standard." + "label": "Tryckmål för avisering", + "doc": "Vart ett tryck på en avisering från den här apparaten öppnar, på Android och iOS. Tomt öppnar WashData-panelen på den här apparaten. Ange en sökväg till en instrumentpanel (t.ex. /lovelace/laundry) eller en fullständig URL för att skicka det någon annanstans, eller \"none\" för inget tryckmål alls." }, "door_opens_at_end": { "label": "Dörren öppnas automatiskt vid avslut", @@ -1617,6 +1651,10 @@ "profile_evidence_sources": { "label": "Cykler som formar ett program", "doc": "Vilka cykler som används för att bygga upp effektkurvan för varje program och för att matcha en avslutad cykel mot den. Avmarkerar du en typ formar den inte längre dina program, utan att något tas bort - cyklerna ligger kvar i din Cykler-lista och kan fortfarande märkas eller tas bort. Praktiskt om du inte litar på importerade data. Statistiken påverkas inte: den räknar alltid bara de cykler som den här maskinen verkligen har kört. Att avmarkera allt ignoreras, eftersom ett program utan cykler bakom sig aldrig skulle kunna matcha." + }, + "energy_price_dynamic": { + "label": "Tidsviktad kostnad", + "doc": "Debitera varje cykel till det pris som gällde i varje ögonblick den kördes, i stället för det enda pris som gällde när den var klar. Gäller bara en prisentitet (ett statiskt pris kan inte röra sig). Kräver ingen extra inställning: priset följs live medan cykeln kör, och \"Bearbeta historik\" under Diagnostik räknar om kostnaden för äldre cykler från recordern. Av = det klassiska beteendet, ett pris fryst vid slutet." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Delar upp…", "deleting": "Raderar…", "imported": "Importerad", - "preparing": "Förbereder…" + "preparing": "Förbereder…", + "unlabelling": "Tar bort märkning…" }, "suggestion": { "both_agree": "WashData rekommenderar", @@ -1775,10 +1814,6 @@ "higher": "Bara mycket säkra matchningar bekräftas – fler cykler förblir omärkta", "lower": "Fler cykler matchas – vissa kan vara något felaktiga" }, - "running_dead_zone": { - "higher": "Korta effekttoppar under tomgång ignoreras – mindre brusig", - "lower": "Reagerar på kortare effektimpulser – fångar snabba transienter" - }, "start_threshold_w": { "higher": "Undviker falska starter från korta effekttoppar", "lower": "Fångar lågeffektsprogram – kan triggas av brus" @@ -1803,6 +1838,8 @@ "end_repeat_count": "{false}/{total} rena cykler ({pct}%) hade en intern paus på >60s som återupptogs; kräver {val} slutbekräftelse(r).", "learning_confidence": "p05-konfidens för {cycles} användarmärkta cykler ({p05}); under detta begärs verifiering.", "match_interval": "Baserat på observerad uppdateringstakt (median={median}s) * 10.", + "match_interval_capped": "Begränsad så att {persistence} matchningar i följd får plats inom {pct}% av det kortaste programmet ({shortest}s); enbart uppdateringstakten (median={median}s) hade föreslagit ett längre intervall.", + "match_interval_floored": "Bara det kortaste programmet ({shortest}s) skulle begränsa detta till {cap}s, under minimigränsen på {minimum}s, så det hålls kvar där: {persistence} matchningar i följd tar {budget}s, vilket är mer än {pct}% av det programmet. Matchning körs bara när en mätning kommer in (median={median}s), så minimivärdet kostar ingenting.", "max_duration_ratio": "Baserat på varaktigheter för märkta cykler (p95={p95}).", "min_active": "Baserat på lägsta aktiva effekt ({min}W) som observerades i senaste cykeln.", "min_duration_ratio": "Hålls så låg som möjligt så att ett program känns igen tidigt i cykeln; konfidens- och tvetydighetsgränserna förhindrar förhastade beslut.", @@ -1921,6 +1958,7 @@ "recording_stopped": "Inspelning stoppad", "review_saved": "Granskning sparad", "settings_conflicts": "Åtgärda de markerade inställningskonflikterna innan du sparar.", + "settings_no_changes": "Inga ändringar att spara", "settings_reverted": "Inställningar återställda; integration laddas om", "settings_saved": "Inställningar sparade; integration laddas om", "suggestions_applied": "Förslag tillämpade; integration laddas om", @@ -1979,7 +2017,11 @@ "export_selective_done": "Export nedladdad", "import_selective_done": "Importerade {profiles} profil(er) och {cycles} cykel/cykler", "hist_csv_required": "Ladda in en CSV-fil eller klistra in dess innehåll först", - "file_read_failed": "Kunde inte läsa den filen" + "file_read_failed": "Kunde inte läsa den filen", + "unlabel_done": "Märkningen togs bort från {count} cykler", + "unlabel_failed": "Borttagning av märkning misslyckades: {error}", + "odometer_saved": "Totalt antal cykler sparat", + "odometer_save_failed": "Kunde inte spara totalen: {error}" }, "trend": { "down": "Nedåtgående trend", @@ -1998,10 +2040,11 @@ "date_tip": "Datum och tid då cykeln startade.", "duration_tip": "Total körtid för cykeln från start till slut.", "energy_tip": "Total förbrukad energi (kWh). Beräknad genom att integrera effekten över tid.", - "cost_tip": "Energikostnad för denna cykel, låst vid slutförandet utifrån priset som gällde då (energi x pris per kWh). Ange ett pris under Inställningar för att fylla i den.", + "cost_tip": "Energikostnad för denna cykel, fryst vid slutförandet. Med Tidsviktad kostnad på debiteras effektkurvan till det pris som gällde i varje ögonblick, annars energi x det enda pris som gällde vid slutet. Ange ett pris under Inställningar för att fylla i den.", "confidence_tip": "Profilmatchkonfidens (0-100 %). Hur nära cykelns effektkurva matchade det identifierade programmet.", "health_tip": "ML-cykelhälsa (högre = bättre). Klicka på en cykel för att inspektera och granska den.", - "flags_tip": "Gransknings-, anomali- och källflaggor för cykeln. Håll muspekaren över en ikon för detaljer." + "flags_tip": "Gransknings-, anomali- och källflaggor för cykeln. Håll muspekaren över en ikon för detaljer.", + "cost_dynamic_tip": "Tidsviktad: debiterad till det pris som gällde under cykeln (effektivt {price} {cur}/kWh)." }, "stat": { "avg": "snitt", @@ -2028,9 +2071,9 @@ "finished": "Cykeln nådde ett sluttillstånd och avslutades." }, "store": { + "search_ph2": "Sök efter märke eller modell…", "your_model_tip": "Det här är apparaten du har angett i Inställningar", "your_model": "Din apparat", - "search_brand_ph": "Sök efter märke…", "programs_count": "Program: {n}", "browse": "Bläddra", "device": "Enhet", @@ -2075,7 +2118,8 @@ "golden": "Bearbetar igen: fyller i referenscykler", "suggestions": "Bearbetar igen: förslag", "ml_training": "Bearbetar igen: ML-träning", - "health": "Bearbetar igen: cykelhälsa" + "health": "Bearbetar igen: cykelhälsa", + "costs": "Bearbetar igen: energikostnader" }, "pg_sweep": { "optimize": "Optimera: {param}" diff --git a/custom_components/ha_washdata/translations/panel/tr.json b/custom_components/ha_washdata/translations/panel/tr.json index aa3574e6..4e7a81e0 100644 --- a/custom_components/ha_washdata/translations/panel/tr.json +++ b/custom_components/ha_washdata/translations/panel/tr.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Sona yaklaşırken bu program yaklaşık {mins} dk sessizleşir, ardından bitmeden önce {secs} sn boyunca yaklaşık {watts} W çeker. Ölçülen {measured} döngüden {seen} tanesinde görüldü: cihaz bunu her çalışmada yapmaz.", + "quiet_tail": "~{mins} dk sessiz bitiş", "artifact_tip": "{n} anormallik algılandı (ör. döngünün ortasında kapının açılması) – bunları grafikte görmek için açın", "auto": "(otomatik algılandı)", "built_in_tag": "yerleşik", @@ -77,11 +79,14 @@ "approved": "Onaylandı", "awaiting": "Onay bekleniyor", "imported_tip": "Topluluk mağazasından içe aktarıldı. Yalnızca eşleştirme için kullanılır, istatistiklere sayılmaz.", + "unmatchable": "eşleştirilemez", "not_importable": "burada geçersiz", "exists": "mevcut", - "backfilled_tip": "İçe aktarılan güç geçmişinde algılandı. Yalnızca program eşleştirmesini etkiler, istatistiklere sayılmaz." + "backfilled_tip": "İçe aktarılan güç geçmişinde algılandı. Yalnızca program eşleştirmesini etkiler, istatistiklere sayılmaz.", + "armed": "(sonraki döngüye uygulanır)" }, "btn": { + "reset_zoom": "Yakınlaştırmayı sıfırla", "set_brand_model": "Marka ve modeli ayarla", "refresh_catalog": "Kataloğu yenile", "add_device": "+ Cihaz ekle", @@ -215,6 +220,8 @@ "trim": "Kırp", "trim_split": "Kırp / Böl", "undo": "Geri al", + "unlabel_selected": "Seçilenlerin etiketini kaldır ({n})", + "unlabel_selected_tip": "Seçili döngülerden profil etiketini kaldırır. Döngüler geçmişinizde kalır ve daha sonra tekrar etiketlenebilir - yalnızca bu profili şekillendirmeyi bırakırlar.", "use": "Kullan", "wipe_all": "Tüm Verileri Sil", "delete_selected": "Seçilenleri sil ({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "En iyisini uygula", "pg_apply_to_settings": "Ayarlara kaydet", "pg_apply_to_settings_tip": "Burada düzenlediğiniz değerleri bu cihazın etkin ayarlarına kopyalayın", - "search": "Ara", "import": "İçe aktar", "disconnect": "Bağlantıyı kes", "connect_github": "GitHub'a bağlan", @@ -249,7 +255,8 @@ "hist_read_recorder": "Home Assistant'tan oku", "hist_scan": "Döngüleri tara", "hist_import_n": "{n} döngüyü içe aktar", - "hist_goto_cycles": "Döngüleri göster" + "hist_goto_cycles": "Döngüleri göster", + "save_total_cycles": "Toplamı Kaydet" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Kaydetmeden önce çakışmaları düzeltin", "attn_title": "Ayar çakışmaları: {n}", + "price_entity": { + "power_sensor": "Bu, cihazın Güç Sensörü; bir fiyat değil. Fiyat varlığı aşağıdaki sabit fiyatı geçersiz kılar, bu yüzden döngüler watt değeri kWh başına fiyatmış gibi ücretlendirilir - bu alanı temizleyin.", + "energy_sensor": "Bu, cihazın Enerji Sayacı; bir fiyat değil. Fiyat varlığı aşağıdaki sabit fiyatı geçersiz kılar, bu yüzden döngüler sayaç okuması kWh başına fiyatmış gibi ücretlendirilir - bu alanı temizleyin.", + "device_class": "Bu sensör {dc} ölçer, kWh başına fiyat değil. Fiyat varlığı aşağıdaki sabit fiyatı geçersiz kılar - bu alanı temizleyin veya bir tarife sensörüne yönlendirin.", + "unit": "Bu sensör {unit} biriminde okuma verir; bu da kWh başına fiyat değildir. Fiyat varlığı aşağıdaki sabit fiyatı geçersiz kılar - bu alanı temizleyin veya bir tarife sensörüne yönlendirin." + }, "settings_banner": "Ayar çakışmaları: {n}. Vurgulanan bölümleri kontrol edin ve kaydetmeden önce düzeltin.", "settings_banner_btn": "İlkine git", "confidence": { @@ -349,7 +362,7 @@ "pg_settings_source": "Ayarların kaynağı", "phase_catalog": "Aşama Kataloğu", "playground": "Deneme Alanı", - "process_history": "İşlem Geçmişi", + "process_history": "Geçmişi İşle", "settings_history": "Ayar geçmişi", "status": "Durum", "status_graph": "Durum Grafiği", @@ -360,12 +373,12 @@ "wipe_history": "Geçmişi Sil", "recommendations": "Öneriler ({n})", "pg_detection_params": "Algılama ayarları", - "pg_timeline": "Olay zaman çizelgesi", "pg_outcome": "Simülasyon sonucu", "pg_across_cycles": "Tüm döngülerinizde", "community_store": "Topluluk Mağazası", "online_account": "Topluluk Mağazası ve çevrimiçi özellikler", - "import_power_history": "Güç geçmişini içe aktar" + "import_power_history": "Güç geçmişini içe aktar", + "service_status": "Bakım Durumu" }, "health": { "fair": "Kabul edilebilir profil kalitesi", @@ -373,6 +386,10 @@ "poor": "⚠ Düşük profil kalitesi" }, "lbl": { + "envelope_position_tip": "Eşleşen programın kendi kayıtlı eğrisi üzerindeki konum, %{pct}. Zaman saymak yerine bu döngü profile hizalanarak ölçülür, böylece çalışma her zamankinden uzun ya da kısa olduğunda da doğru kalır. Cihaz sessizken yenilenir, bu yüzden etkin bir aşama sırasında geride kalabilir.", + "envelope_position": "eğri %{pct}", + "aria_playground_chart3": "Etkileşimli döngü güç grafiği: zamanı/gücü okumak için üzerine gelin, yakınlaştırmak için kaydırın veya iki parmak kullanın, kaydırmak için sürükleyin", + "device_unresolved": "Kullanılamayan cihaz ({id})", "drag_to_resize": "Yeniden boyutlandırmak için sürükleyin", "actions": "Eylemler", "activity": "Aktivite", @@ -508,7 +525,7 @@ "profile_index": "profil dizini →", "profile_name": "Profil Adı", "program": "Program", - "program_tip": "Geçerli döngüyle eşleştirilen profili geçersiz kılın. Otomatik algılama, entegrasyonun en iyi eşleşmeyi otomatik olarak seçmesine olanak tanır. Otomatik algılama yanlış olduğunda veya neyin çalıştığını bildiğinizde belirli bir programı zorla eşleştirmek için sabitleyin.", + "program_tip": "Geçerli döngüyle eşleştirilen profili geçersiz kılın. Otomatik algılama, entegrasyonun en iyi eşleşmeyi otomatik olarak seçmesine olanak tanır. Otomatik algılama yanlış olduğunda veya neyin çalıştığını bildiğinizde belirli bir programı zorla eşleştirmek için sabitleyin. Cihazı çalıştırmadan önce bir program seçin; sonraki döngü başladığı anda uygulanır.", "progress": "İlerleme", "raw_socket": "Ham soket", "read_only": "Salt okunur", @@ -543,6 +560,7 @@ "stage4": "Aşama 4 – uyum", "status": "Durum", "tail_trim": "Sondan Kırpma (s)", + "tap_to_dismiss": "kapatmak için grafiğe dokunun", "timer_auto_pause": "Otomatik duraklatma", "timer_min": "dk.", "timer_msg_placeholder": "Mesaj (isteğe bağlı, {device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "Kalan süre (model)", "pg_time_left_model_tip": "Model tarafından tahmin edilen kalan süre (aşama tahmincisi + ML karışımı), sabit bir geri sayım değil", "phase": "Aşama", - "pg_mode_simulate": "Simülasyon", "pg_mode_history": "Geçmişte test et", - "pg_mode_sweep": "Tarama", "pg_ev_detected": "Algılandı", "pg_ev_match": "Eşleşme kesinleşti", "pg_ev_match_changed": "Eşleşme değişti", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "Otomatik algılanan bekleme tabanını geçersiz kıl (otomatik için boş bırakın).", "pg_stress_toggle": "Bekleme sonlandırmasını test et", "pg_stress_toggle_desc": "Kaydın bitmesinin ardından cihazın bekleme modundaki güç tüketimine devam ettiğini simüle eder - WashData'nın döngüyü durdurup durdurmayacağını ve ne zaman durduracağını gösterir.", - "pg_sweep_2d": "2D ısı haritası", - "pg_sweep_param_y": "2. parametre", "pg_obj_match": "Eşleşme doğruluğu", "pg_obj_endtiming": "Bitiş zamanı doğruluğu", "pg_obj_falseend": "Yanlış bitiş oranı", "pg_obj_overrun": "Süre sapması", "pg_obj_ambiguity": "Belirsizlik oranı", - "pg_best_combo": "en iyi kombinasyon", "pg_mode_optimize": "Optimize et", "pg_group_matching": "Program eşleştirme", "pg_zoom_hint": "yakınlaştırmayı sıfırlamak için çift tıklayın", @@ -706,7 +719,12 @@ "task_history_import_apply": "Döngüleri içe aktarma", "evidence_real_cycles": "Bu makinenin çalıştırdığı döngüler", "evidence_reference_cycles": "Topluluk mağazasından indirilenler", - "evidence_backfill_cycles": "İçe aktarılan güç geçmişinde bulunanlar" + "evidence_backfill_cycles": "İçe aktarılan güç geçmişinde bulunanlar", + "zoom_hint_touch": "iki parmakla yakınlaştırın", + "zoom_hint_touch_zoomed": "iki parmakla yakınlaştırın · sıfırlamak için çift dokunun", + "cycles_since_service": "{since} / {total} döngü", + "total_cycles_run": "toplamda çalıştırılan döngü", + "correct_total_cycles": "Toplamı düzelt" }, "log": { "all_levels": "Tüm seviyeler", @@ -756,7 +774,7 @@ "merge_cycles": "{n} Döngüyü Birleştir", "new_group": "Yeni profil grubu", "new_phase": "Yeni Aşama", - "process_history_msg": "Tüm saklanan döngülerde eşleştirmeyi yeniden çalıştırın, önerileri yenileyin, ML'yi yeniden eğitin (etkinleştirilmişse) ve döngü sağlığını yeniden hesaplayın. Bu biraz zaman alabilir.", + "process_history_msg": "Tüm saklanan döngülerde eşleştirmeyi yeniden çalıştırın, önerileri yenileyin, ML'yi yeniden eğitin (etkinleştirilmişse), döngü maliyetlerini kayıtlı enerji fiyatlarına göre yeniden hesaplayın ve döngü sağlığını yeniden hesaplayın. Bu biraz zaman alabilir.", "process_history_ok": "İşle", "process_history_title": "Geçmişi İşle", "process_recording": "Kaydı İşle", @@ -789,6 +807,7 @@ "history_import": "Güç geçmişini içe aktar" }, "msg": { + "pg_canvas_empty3": "Yukarıdan bir döngü seçin ve simüle etmek için Çalıştır'a basın. Ardından değerleri okumak için üzerine gelin, yakınlaştırmak için kaydırın veya iki parmak kullanın ve kaydırmak için sürükleyin.", "tail_trim_hint": "Sondan kaldırılacak saniye sayısı", "store_sibling_hint": "Tam olarak sizin modeliniz için paylaşılmış bir şey yok mu? Aynı markanın yakın bir modeli genellikle iyi bir başlangıç noktasıdır.", "store_declare_appliance": "WashData'ya hangi cihaza sahip olduğunuzu bildirin; bu sekme, başkalarının o cihaz için paylaştığı programları ve kayıtları gösterir. Ayrıca yukarıya bir marka yazıp göz atabilirsiniz.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Normal güç bandının üzerinde yaklaşık ~{n} saniye sürdü.", "auto_label_intro": "Eşleşme güveni eşiği geçen etiketsiz döngülere profil atayın.", "automations_intro": "WashData, {start} / {end} olaylarını tetikler ve varlıkları gösterir; bu nedenle bildirimler ve eylemler en iyi şekilde normal Home Assistant otomasyonları olarak oluşturulur. Bu cihazı kullanan otomasyonlar aşağıda görünür.", - "cleanup_intro": "Tüm etiketli döngüler üst üste bindirilmiş. Aykırı değerleri işaretleyip silerek profili temizleyin.", + "cleanup_intro": "Tüm etiketli döngüler üst üste bindirilmiş. Aykırı değerleri işaretleyin, ardından etiketlerini kaldırın (geçmişte kalırlar ama artık bu profili şekillendirmezler) veya tamamen silin.", "clear_debug_hint": "Saklanan hata ayıklama verilerini kaldırarak boş alan açın.", "collecting_data": "Veri toplanıyor. İnce ayarın başlaması için gereken döngü sayısı: {need} ({current}/{min}).", "compare_overlay_profiles": "Profilleri üst üste bindirin (soluk)", @@ -901,7 +920,7 @@ "preferences_admin": "Tercihler, panel ve erişim denetimi", "preferences_adv": "Tercihler", "prefs_personal": "Bunlar yalnızca Home Assistant hesabınıza uygulanır.", - "process_history_hint": "Tüm saklanan döngülerde eşleştirmeyi yeniden çalıştırın, ayar önerilerini yenileyin, ML modellerini yeniden eğitin (etkinleştirilmişse) ve döngü sağlığını yeniden hesaplayın. Bir toplu incelemenin ardından çalıştırın.", + "process_history_hint": "Tüm saklanan döngülerde eşleştirmeyi yeniden çalıştırın, ayar önerilerini yenileyin, ML modellerini yeniden eğitin (etkinleştirilmişse), döngü maliyetlerini kayıtlı enerji fiyatlarına göre yeniden hesaplayın ve döngü sağlığını yeniden hesaplayın. Bunu bir grup incelemenin ardından çalıştırın.", "profile_deleted": "Profil silindi", "profile_poor_health_detail": "Bu profile atanan döngüler tutarsız şekillere sahip veya güvenirliği düşük. Zarfı yeniden oluşturmayı veya etiketli döngüleri gözden geçirmeyi düşünün.", "profiles_intro": "İstatistikler, aşamalar ve temizleme için bir profile tıklayın. Güvenilir seçim için neredeyse aynı programları (aynı şekil/süre, farklı sıcaklık veya dönüş) gruplayın.", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' düşük bir uyum puanına sahip - son çevrimleri çok değişkenlik gösteriyor veya zayıf eşleşiyor. Eşleştirme ve süre tahminlerinin doğru kalması için çevrimlerini gözden geçirin ya da profili yeniden kaydedin.", "advisory_shape_drift": "'{name}' orijinal güç eğrisinden önemli ölçüde uzaklaştı. Cihazın davranışı zamanla değişmiş olabilir (ör. kireçlenme, aşınma). Bu profili son çevrimlerle yeniden kaydetmeyi düşünün.", "advisory_shape_drift_corr": "'{name}' orijinal güç eğrisinden önemli ölçüde uzaklaştı (korelasyon {corr}). Cihazın davranışı zamanla değişmiş olabilir (ör. kireçlenme, aşınma). Bu profili son çevrimlerle yeniden kaydetmeyi düşünün.", + "advisory_unmatchable": "'{name}' asla eşleştirilemez: arkasında güç verisi bulunan bir çevrim yok, bu nedenle WashData çalışan bir çevrimi karşılaştıracak bir referansa sahip değil. Profili yeniden eşleştirmeye dahil etmek için bu programın bir çevrimini etiketleyin (veya bir çevrim kaydedin).", + "advisory_unmatchable_no_duration": "'{name}' asla eşleştirilemez: güç verisi bulunan bir çevrimi var ancak kullanılabilir bir süresi yok, bu nedenle WashData eşleştirmeyi ölçeklendirecek bir süreye sahip değil. Bu programı yeniden kaydedin veya beklenen süresini girin; böylece profil yeniden eşleştirmeye dahil olur.", "pg_no_alerts": "Bu çalıştırmada sorun tespit edilmedi.", "pg_override_active": "Düzenlediğiniz ayarlar mevcut ayarlara karşı kullanılıyor", - "pg_history_intro": "Son döngülerinizi gerçek algılayıcı ve eşleştirici üzerinden yeniden oynatın. Burada bir önce/sonra karşılaştırması görmek için önce Simülasyon modunda bir algılama ayarını düzenleyin.", "pg_history_empty": "Döngülerinizi yeniden oynatmak için Çalıştır'a basın.", "pg_history_summary": "{detected}/{total} algılandı · {correct} doğru eşleşti", "pg_sweep_no_metric": "Bu hedefi puanlamak için yeterli veri yok.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Tarama başarısız oldu.", "hist_scan_expired": "Bu tarama artık kullanılamıyor. Lütfen yeniden tarayın.", "hist_import_failed": "İçe aktarma başarısız oldu.", - "imported_history_readonly": "İçe aktarılan güç geçmişinde algılandı. Program eşleştirmesini etkiler ancak istatistiklerinize sayılmaz, kırpılamaz veya bölünemez. Programını adlandırmak için etiketleyin." + "imported_history_readonly": "İçe aktarılan güç geçmişinde algılandı. Program eşleştirmesini etkiler ancak istatistiklerinize sayılmaz, kırpılamaz veya bölünemez. Programını adlandırmak için etiketleyin.", + "odometer_intro": "Bu toplam yalnızca artar. Bir döngü kaydını silmek onu değiştirmez ve saklanan geçmiş sınırının ötesinde saymaya devam eder, böylece servis hatırlatıcıları doğru kalır.", + "odometer_correct_hint": "WashData gerçekleşmemiş bir çalışma kaydettiyse veya cihaz WashData kurulmadan önce zaten döngü çalıştırmışsa bu değeri ayarlayın.", + "no_reminders_set": "Henüz servis hatırlatıcısı ayarlanmadı.", + "toast_program_armed": "Program sonraki döngü için ayarlandı: {program}" }, "pg_desc": { "completion_min_seconds": "Gerçek döngü sayılan en kısa çalışma", @@ -1093,7 +1117,9 @@ "duration_scale": "Aşama 4: süre uyumunun yarılandığı logaritmik oran; küçük = daha sert ceza (varsayılan 0.175)", "energy_scale": "Aşama 4: enerji uyumunun yarılandığı logaritmik oran; küçük = daha sert ceza (varsayılan 0.25)", "dishwasher_end_spike_quiet_release": "Bulaşık makinesi: beklenen süreden sonra döngü sonu tahliye beklemesi bırakılmadan önceki sessiz saniyeler", - "smart_termination_duration_ratio": "Eşleşen programın beklenen süresinin, Akıllı Sonlandırma'nın döngüyü erken bitirebilmesi için döngünün ulaşması gereken kesri; yüke veya sıcaklığa bağlı makinelerde düşürün" + "smart_termination_duration_ratio": "Eşleşen programın beklenen süresinin, Akıllı Sonlandırma'nın döngüyü erken bitirebilmesi için döngünün ulaşması gereken kesri; yüke veya sıcaklığa bağlı makinelerde düşürün", + "anti_crease_finalize_ratio": "Kırışıklık önleme tambur kuyruğunun sonlandırılabilmesi için döngünün ulaşması gereken, eşleşen programın beklenen süresine oranı; uzun tambur kuyruğu olan kurutucularda düşürün, çamaşır makinelerini varsayılanda bırakın", + "curve_preroll_seconds": "İptal edilen başlatma denemelerinden bir döngünün eğrisinin başına taşınan okuma saniyesi; 0 = kapalı" }, "phase_desc": { "anti_crease": "Kırışıklıkları azaltmak için tamamlandıktan sonra ara sıra kısa yuvarlanmalar.", @@ -1208,7 +1234,7 @@ "label": "Maks. Kırışıklık Önleme Gücü" }, "auto_label_confidence": { - "doc": "Döngü sonundaki maç skoru bu seviyede veya üzerindeyse program herhangi bir onay istemi olmadan otomatik olarak etiketlenir. Otomatik etiketlemeden önce daha yüksek kesinlik gerektirecek şekilde yükseltin; daha fazlasını otomatikleştirmek için düşürün. Altındaki Learning Confidence ile birlikte çalışır.", + "doc": "Döngü sonundaki eşleşme puanı bu seviyede veya üzerindeyse program herhangi bir onay istemi olmadan otomatik olarak etiketlenir. Otomatik etiketlemeden önce daha yüksek kesinlik gerektirmek için yükseltin; daha fazlasını otomatikleştirmek için düşürün. Hemen altındaki Öğrenme Güveni ile birlikte çalışır.", "label": "Otomatik Etiket Güveni" }, "auto_maintenance": { @@ -1256,7 +1282,7 @@ "label": "Bitiş Tekrar Sayısı" }, "energy_price_entity": { - "doc": "kWh başına güncel elektrik fiyatını gösteren sensör (örn. dinamik tarife). Aşağıdaki statik fiyata göre önceliklidir. Her döngü bittiğinde geçerli olan fiyat dondurulur.", + "doc": "kWh başına güncel elektrik fiyatını gösteren sensör (örn. dinamik tarife). Bir fiyat olmalıdır, prizin kendi güç veya enerji varlığı değil: aşağıdaki statik fiyata göre önceliklidir, bu nedenle buraya konan bir kWh sayacı her döngüyü tarifeniz yerine sayaç okumasıyla ücretlendirir. Zaman Ağırlıklı Maliyet açıkken her döngü, çalıştığı her an geçerli olan fiyattan ücretlendirilir; aksi halde bittiğinde geçerli olan fiyat üzerine dondurulur.", "label": "Enerji Fiyatı Varlığı" }, "energy_price_static": { @@ -1284,7 +1310,7 @@ "label": "Harici Tetikleyiciyi Tersine Çevir (KAPALI'da tetikle)" }, "learning_confidence": { - "doc": "Eşleşme puanı bu ile Otomatik Etiket Güveni arasında kalırsa, bir geri bildirim bildirimi tanımlanan programı doğrulamanızı ister. Bu skorun altında maç yüzeye çıkamayacak kadar belirsiz. Otomatik Etiket Güveninin altında tutulmalıdır.", + "doc": "Eşleşme puanı bu değer ile Otomatik Etiket Güveni arasında kalırsa, bir geri bildirim bildirimi tanımlanan programı doğrulamanızı ister. Bu puanın altında eşleşme gösterilemeyecek kadar belirsizdir. Otomatik Etiket Güveninin altında tutulmalıdır.", "label": "Öğrenme Güveni" }, "linked_device": { @@ -1352,7 +1378,7 @@ "label": "Bildirimler için HA Olayları Tetikle" }, "notify_icon": { - "doc": "Bildirim için isteğe bağlı mdi simgesi (ör. mdi:çamaşır makinesi). Platform varsayılanı için boş bırakın.", + "doc": "Bildirimde gösterilen küçük simge (ör. mdi:washing-machine). Android bunu durum çubuğuna çizer; iOS ise uygulama simgesinin yerine gösterir ve 2026.8 veya daha yeni bir tamamlayıcı uygulama gerektirir. Platform varsayılanı için boş bırakın.", "label": "Bildirim Simgesi" }, "notify_live_chronometer": { @@ -1428,7 +1454,7 @@ "label": "Güç Kapatma Eşiği" }, "power_sensor": { - "doc": "Bu cihaz için canlı gücü watt cinsinden bildiren sensör varlığı (ör. sensör.washer_power). Tüm döngü tespiti bu sinyale dayanır.", + "doc": "Bu cihaz için canlı gücü watt cinsinden bildiren sensör varlığı (ör. sensor.washer_power). Tüm döngü tespiti bu sinyale dayanır.", "label": "Güç Sensörü" }, "profile_duration_tolerance": { @@ -1452,7 +1478,7 @@ "label": "Eşleşme Eşiği" }, "profile_unmatch_threshold": { - "doc": "Canlı bir döngü ortası maçı bu puanın altına düşerse, geçici tanımlama silinir. Eşleşme Eşiğinin biraz altında tutun, böylece benzerlikteki kısa bir düşüş, ekranı tekrar benzersiz hale getirmez.", + "doc": "Canlı bir döngü ortası eşleşme puanı bu puanın altına düşerse, geçici tanımlama silinir. Eşleşme Eşiğinin biraz altında tutun, böylece benzerlikteki kısa bir düşüş ekranı yeniden eşleşmedi durumuna döndürmez.", "label": "Eşleşmeme Eşiği" }, "progress_reset_delay": { @@ -1463,10 +1489,6 @@ "doc": "Bir pompa, muhtemelen sıkışmış olarak işaretlenmeden önce sürekli olarak çalışabilir (sıkışmış pompa olayını tetikler).", "label": "Pompa Takılma Süresi" }, - "running_dead_zone": { - "doc": "Bir döngü başladıktan sonra bu penceredeki güç düşüşleri dikkate alınmaz. Çamaşır makineleri soğuk suyla doluyor (ısıtmadan önce neredeyse 0 W'a düşüyor) - bu koruma olmadan dolum aşaması bir döngü sonu gibi görünür. Bu, verileri atlamaz: tam güç takibi T=0'dan itibaren kaydedilir. Öneri motoru, makinenizin gerçek başlatma düzenini ölçer ve bunu otomatik olarak boyutlandırır.", - "label": "Çalışma Ölü Bölgesi" - }, "sampling_interval": { "doc": "Sensör okumaları arasında beklenen süre - yumuşatma penceresini boyutlandırmak ve geri dönmeyi doğru şekilde başlatmak için kullanılır. Her sensör güncellemesi bu değere bakılmaksızın yakalanır; yalnızca aşağı yöndeki hesaplamaları kalibre eder. Öneri motoru, sensörünüzün geçmiş döngülerdeki gerçek temposunu ölçer ve bunu otomatik olarak ayarlar.", "label": "Örnekleme Aralığı" @@ -1594,13 +1616,17 @@ "label": "Güç Profili Aralığı", "doc": "EMHASS ve tibber_prices gibi harici planlayıcılar tarafından kullanılan düz slot başına ortalama watt dizisi olan power_profile sensör özelliği için kova boyutu. Küçük kovalar kısa güç ani artışlarını keskin tutar; büyük kovalar şekli düzler. Varsayılan 15 dakika. Yalnızca okuma zamanında kullanılır; algılamayı etkilemez." }, + "notify_live_silent": { + "label": "Sessiz Canlı Güncellemeler", + "doc": "Yalnızca iOS. Canlı ilerlemeyi sessizce yeniler: her güncelleme ses veya titreşim olmadan, pil tasarrufu sağlayan daha düşük bir öncelikle gelir ve iOS bunları kısa süreliğine gruplayabilir. Bir döngünün ilk güncellemesi (Live Activity'yi başlatan güncelleme) ile başlangıç/bitiş bildirimleri her zamanki gibi uyarı vermeye devam eder. Her güncellemede uyarı almak için bunu kapatın." + }, "notify_live_sticky": { "label": "Dokunuşta Canlı Bildirimi Koru", "doc": "Yalnızca Android. Canlı ilerleme bildirimini kalıcı (yapışkan) hale getirerek üzerine dokunulduğunda devam eden bildirimin kapatılmasını önleyin. Kapalı olduğunda dokunmanın bildirimi kapattığı varsayılan davranış korunur." }, "notify_live_click_action": { - "label": "Canlı Bildirim Dokunma Hedefi", - "doc": "Yalnızca Android. Canlı ilerleme bildirimine dokunulduğunda açılan yer (örn. /lovelace/laundry veya tam URL), uygulama açılış sayfası yerine. Varsayılan için boş bırakın." + "label": "Bildirim Dokunma Hedefi", + "doc": "Bu cihazdan gelen bir bildirime dokunulduğunda nereye gidileceği, hem Android hem de iOS'ta. Boş bırakılırsa WashData paneli bu cihazda açılır. Başka bir yere yönlendirmek için bir pano yolu (ör. /lovelace/laundry) veya tam bir URL girin; hiç dokunma hedefi olmaması için \"none\" yazın." }, "door_opens_at_end": { "label": "Döngü Sonunda Kapı Otomatik Açılır", @@ -1617,6 +1643,18 @@ "profile_evidence_sources": { "label": "Bir programı şekillendiren döngüler", "doc": "Her programın güç eğrisini oluşturmak ve tamamlanmış bir döngüyü onunla eşleştirmek için hangi döngülerin kullanılacağı. Bir türün işaretini kaldırmak, hiçbir şey silmeden onun programlarınızı şekillendirmesini durdurur - döngüler Döngüler listenizde kalır ve yine etiketlenebilir veya kaldırılabilir. İçe aktarılan verilere güvenmiyorsanız kullanışlıdır. İstatistikler etkilenmez: her zaman yalnızca bu makinenin gerçekten çalıştırdığı döngüleri sayarlar. Her şeyin işaretini kaldırmak yok sayılır, çünkü arkasında hiç döngü olmayan bir program asla eşleşemez." + }, + "energy_price_dynamic": { + "label": "Zaman Ağırlıklı Maliyet", + "doc": "Her döngüyü, bittiğinde geçerli olan tek fiyat yerine, çalıştığı her an geçerli olan fiyattan ücretlendirir. Yalnızca fiyat varlığı için geçerlidir (sabit fiyat değişemez). Ek kurulum gerektirmez: döngü çalışırken fiyat canlı olarak izlenir, Tanılamalar altındaki Geçmişi İşle eylemi ise eski döngülerin maliyetini recorder verilerinden yeniden hesaplar. Kapalı = klasik davranış, sonda dondurulan tek fiyat." + }, + "anti_crease_finalize_ratio": { + "label": "Kırışıklık Önleme Sonlandırma Oranı", + "doc": "WashData'nın gördüğü düşük güçlü tamburlamanın kırışıklık önleme kuyruğu olduğuna karar verip döngüyü orada bitirebilmesi için, döngünün eşleşen programın beklenen süresinin ne kadarına ulaşmış olması gerektiği. Beklenen süre, programın ortalamasıdır; bu yüzden sensörlü kurutma süresi çamaşır miktarına göre değişen bir kurutucuda çoğu çalışma bu sürenin %98'ine hiç ulaşmaz ve kuyruk, yedek zaman aşımına kadar bekler; bu da çoğu zaman makine gerçekte durduktan 15 ila 40 dakika sonrasını bulur. Böyle bir kurutucuda bu değeri düşürün (örneğin 0,75). Çamaşır makinelerini varsayılanda bırakın: bir çamaşır makinesi döngüsünün büyük bölümünü Maks. Kırışıklık Önleme Gücü'nün altında geçirir, dolayısıyla sessiz bir yıkama ara aşamasını kuyruktan ayıran tek şey bu orandır ve değeri düşürmek bir yıkamayı ortasından kesebilir. Varsayılan (0,98) için boş bırakın. Bu, ayrı bir erken bitirme denetimini yöneten Akıllı Sonlandırma Oranı'ndan farklı bir ayardır." + }, + "curve_preroll_seconds": { + "label": "Eğri Ön Kaydı", + "doc": "WashData'nın, iptal edilen başlatma denemelerinden gelen kaç saniyelik okumayı bir döngünün eğrisinin başına taşıyabileceği. Bir döngünün eğrisi normalde sonunda başarılı olan başlatma denemesiyle başlar; bu nedenle yerleşmeden önce birkaç kez yoklayan makinelerde (program seçimi, kapak kilidi, ilk su alma) gerçek etkinliğin ilk dakikaları her eğride eksik kalır. Bu değeri, makinenizdeki en uzun hazırlık aşamasının biraz üzerine ayarlayın (çoğu makine için 300 sn fazlasıyla yeter). 0 kapatır. Açmanın kaydedilen başlangıcı öne çektiğini unutmayın: değişiklikten önce ve sonra kaydedilen döngüler aynı program için farklı süreler taşır ve eski döngüler elenene kadar öğrenilen ortalamalar kayar." } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "Bölünüyor…", "deleting": "Siliniyor…", "imported": "İçe aktarıldı", - "preparing": "Hazırlanıyor…" + "preparing": "Hazırlanıyor…", + "unlabelling": "Etiket kaldırılıyor…" }, "tab": { "advanced": "Gelişmiş", @@ -1808,6 +1847,7 @@ "recording_stopped": "Kayıt durduruldu", "review_saved": "İnceleme kaydedildi", "settings_conflicts": "Kaydetmeden önce vurgulanan ayar çakışmalarını düzeltin.", + "settings_no_changes": "Kaydedilecek değişiklik yok", "settings_reverted": "Ayarlar geri yüklendi; entegrasyon yeniden yükleniyor", "settings_saved": "Ayarlar kaydedildi; entegrasyon yeniden yükleniyor", "suggestions_applied": "Öneriler uygulandı; entegrasyon yeniden yükleniyor", @@ -1866,7 +1906,11 @@ "export_selective_done": "Dışa aktarma indirildi", "import_selective_done": "{profiles} profil ve {cycles} döngü içe aktarıldı", "hist_csv_required": "Önce bir CSV dosyası yükleyin veya içeriğini yapıştırın", - "file_read_failed": "Bu dosya okunamadı" + "file_read_failed": "Bu dosya okunamadı", + "unlabel_done": "{count} döngünün etiketi kaldırıldı", + "unlabel_failed": "Etiket kaldırma başarısız: {error}", + "odometer_saved": "Toplam döngü sayısı kaydedildi", + "odometer_save_failed": "Toplam kaydedilemedi: {error}" }, "suggestion": { "both_agree": "WashData öneriyor", @@ -1913,10 +1957,6 @@ "higher": "Yalnızca yüksek güvenli eşleşmeleri onaylar; daha fazla döngü etiketsiz kalır", "lower": "Daha fazla döngüyle eşleşir; bazıları hafif hatalı olabilir" }, - "running_dead_zone": { - "higher": "Boşta kısa güç artışlarını yoksayar – gürültüye daha az duyarlı", - "lower": "Daha kısa güç patlamalarına yanıt verir – hızlı geçici etkinliği yakalar" - }, "start_threshold_w": { "higher": "Kısa güç artışlarından kaynaklanan yanlış başlatmaları önler", "lower": "Düşük güçlü programları yakalar; gürültüde tetiklenebilir" @@ -1962,7 +2002,9 @@ "thr_batch": "{cycles} çevrim boyunca p05 en düşük aktif gücün ({p05}W) hemen üzerinde tutuldu; böylece başlangıç olabildiğince erken yakalanır ve durdurma eşiği makinenin en düşük çalışma gücünün altında kalır.", "tol_per_profile": "{profiles} profil boyunca ({cycles} çevrim) profil başına süre varyansının p75'i; tutarlı profiller cezalandırılmaz.", "tol_pooled": "{cycles} son etiketli çevrimin birleştirilmiş süre varyansına dayalı (p95 sapma={dev}).", - "watchdog": "Güvenli olduğu kadar düşük tutuldu (p95 güncelleme boşluğu {p95}s değerinin hemen üzerinde ve örnekleme aralığı {median}s değerinin en az 2 katı, en az 30s); böylece takılmalar yanlış durdurmalar olmadan hızlıca yakalanır." + "watchdog": "Güvenli olduğu kadar düşük tutuldu (p95 güncelleme boşluğu {p95}s değerinin hemen üzerinde ve örnekleme aralığı {median}s değerinin en az 2 katı, en az 30s); böylece takılmalar yanlış durdurmalar olmadan hızlıca yakalanır.", + "match_interval_capped": "{persistence} ardışık eşleşme en kısa programın ({shortest}s) %{pct} kadarlık bölümüne sığsın diye sınırlandırıldı; yalnızca güncelleme sıklığı (medyan={median}s) daha uzun bir aralık önerirdi.", + "match_interval_floored": "Yalnızca en kısa program ({shortest}s) bunu {cap}s ile sınırlardı; bu da {minimum}s alt sınırının altında kaldığından değer alt sınırda tutuluyor: {persistence} ardışık eşleşme {budget}s sürüyor, bu da o programın %{pct} kadarlık bölümünden fazla. Eşleştirme yalnızca yeni bir ölçüm geldiğinde çalışır (medyan={median}s), bu yüzden alt sınırın bir maliyeti yok." }, "exclusions": { "summary": "{total} yanlış algılanan döngü hariç tutuldu: {parts}.", @@ -1998,10 +2040,11 @@ "date_tip": "Döngünün başladığı tarih ve saat.", "duration_tip": "Baştan sona toplam döngü çalışma süresi.", "energy_tip": "Tüketilen toplam enerji (kWh). Gücün zamana göre integrali alınarak hesaplanır.", - "cost_tip": "Bu döngünün enerji maliyeti; tamamlandığında o an geçerli olan fiyat kullanılarak sabitlenir (enerji x kWh başına fiyat). Doldurmak için Ayarlar altında bir fiyat belirleyin.", + "cost_tip": "Bu döngünün enerji maliyeti, tamamlandığında sabitlenir. Zaman Ağırlıklı Maliyet açıkken güç eğrisi her an geçerli olan fiyattan ücretlendirilir; aksi halde enerji x sonda geçerli olan tek fiyat. Doldurmak için Ayarlar altında bir fiyat belirleyin.", "confidence_tip": "Profil eşleşme güveni (0-100%). Döngünün güç eğrisinin, tanımlanan programla ne kadar yakından eşleştiği.", "health_tip": "ML döngü sağlığı (yüksek = daha iyi). İncelemek ve gözden geçirmek için bir döngüye tıklayın.", - "flags_tip": "Döngü için inceleme, anomali ve kaynak işaretleri. Ayrıntı için bir simgenin üzerine gelin." + "flags_tip": "Döngü için inceleme, anomali ve kaynak işaretleri. Ayrıntı için bir simgenin üzerine gelin.", + "cost_dynamic_tip": "Zaman ağırlıklı: döngü boyunca geçerli olan fiyattan ücretlendirildi (efektif {price} {cur}/kWh)." }, "stat": { "avg": "ort.", @@ -2028,9 +2071,9 @@ "finished": "Döngü, son duruma ulaştı ve sona erdi." }, "store": { + "search_ph2": "Marka veya modele göre ara…", "your_model_tip": "Bu, Ayarlar'da belirttiğiniz cihazdır", "your_model": "Sizinki", - "search_brand_ph": "Markaya göre ara…", "programs_count": "Programlar: {n}", "browse": "Göz at", "device": "Cihaz", @@ -2075,7 +2118,8 @@ "golden": "Yeniden işleme: referans döngülerini işaretleme", "suggestions": "Yeniden işleme: öneriler", "ml_training": "Yeniden işleme: ML eğitimi", - "health": "Yeniden işleme: döngü sağlığı" + "health": "Yeniden işleme: döngü sağlığı", + "costs": "Yeniden işleme: enerji maliyetleri" }, "pg_sweep": { "optimize": "Optimizasyon: {param}" diff --git a/custom_components/ha_washdata/translations/panel/uk.json b/custom_components/ha_washdata/translations/panel/uk.json index 25b4f19e..4ef62ecb 100644 --- a/custom_components/ha_washdata/translations/panel/uk.json +++ b/custom_components/ha_washdata/translations/panel/uk.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "Ближче до кінця ця програма затихає приблизно на {mins} хв, потім споживає близько {watts} Вт протягом {secs} с і завершується. Помічено у {seen}/{measured} виміряних циклів: прилад робить так не за кожного запуску.", + "quiet_tail": "~{mins} хв тиші в кінці", "artifact_tip": "Виявлено {n} аномалій (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити їх на графіку", "auto": "(виявлено автоматично)", "built_in_tag": "вбудований", @@ -77,11 +79,14 @@ "approved": "Схвалено", "awaiting": "Очікує схвалення", "imported_tip": "Імпортовано з магазину спільноти. Використовується лише для зіставлення, не враховується у статистиці.", + "unmatchable": "не зіставляється", "not_importable": "недоступно", "exists": "існує", - "backfilled_tip": "Виявлено в імпортованій історії потужності. Впливає лише на зіставлення програм, не враховується у статистиці." + "backfilled_tip": "Виявлено в імпортованій історії потужності. Впливає лише на зіставлення програм, не враховується у статистиці.", + "armed": "(застосується до наступного циклу)" }, "btn": { + "reset_zoom": "Скинути масштаб", "set_brand_model": "Вказати бренд і модель", "refresh_catalog": "Оновити каталог", "add_device": "+ Додати пристрій", @@ -217,6 +222,8 @@ "trim": "Обрізати", "trim_split": "Обрізати / Розділити", "undo": "Скасувати", + "unlabel_selected": "Зняти мітку з вибраних ({n})", + "unlabel_selected_tip": "Зняти мітку профілю з вибраних циклів. Вони залишаться у вашій історії, і їх можна буде позначити знову пізніше - вони просто перестануть формувати цей профіль.", "use": "Використати", "wipe_all": "Видалити всі дані", "delete_selected": "Видалити вибрані ({n})", @@ -224,7 +231,6 @@ "pg_apply_best": "Застосувати найкраще", "pg_apply_to_settings": "Зберегти в налаштування", "pg_apply_to_settings_tip": "Скопіювати значення, змінені тут, до активних налаштувань цього пристрою", - "search": "Пошук", "import": "Імпортувати", "disconnect": "Від'єднати", "connect_github": "Підключити GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "Зчитати з Home Assistant", "hist_scan": "Знайти цикли", "hist_import_n": "Імпортувати цикли ({n})", - "hist_goto_cycles": "Показати цикли" + "hist_goto_cycles": "Показати цикли", + "save_total_cycles": "Зберегти загальну кількість" }, "conflict": { "anti_wrinkle_exit": { @@ -262,6 +269,12 @@ }, "attn_sub": "Виправте конфлікти перед збереженням", "attn_title": "Конфлікти налаштувань: {n}", + "price_entity": { + "power_sensor": "Це Датчик потужності пристрою, а не ціна. Сутність ціни має пріоритет над фіксованою ціною нижче, тож цикли рахувалися б за ватами за кВт·год - очистіть це поле.", + "energy_sensor": "Це Лічильник енергії пристрою, а не ціна. Сутність ціни має пріоритет над фіксованою ціною нижче, тож цикли рахувалися б за показанням лічильника за кВт·год - очистіть це поле.", + "device_class": "Клас цього сенсора - {dc}, а не ціна за кВт·год. Сутність ціни має пріоритет над фіксованою ціною нижче - очистіть це поле або вкажіть сенсор тарифу.", + "unit": "Цей сенсор видає значення у {unit}, а це не ціна за кВт·год. Сутність ціни має пріоритет над фіксованою ціною нижче - очистіть це поле або вкажіть сенсор тарифу." + }, "settings_banner": "Конфлікти налаштувань: {n}. Перевірте виділені розділи та виправте їх перед збереженням.", "settings_banner_btn": "До першого", "confidence": { @@ -360,12 +373,12 @@ "wipe_history": "Очистити історію", "recommendations": "Рекомендації ({n})", "pg_detection_params": "Налаштування виявлення", - "pg_timeline": "Хронологія подій", "pg_outcome": "Результат симуляції", "pg_across_cycles": "За всіма вашими циклами", "community_store": "Магазин спільноти", "online_account": "Магазин спільноти та онлайн-функції", - "import_power_history": "Імпорт історії потужності" + "import_power_history": "Імпорт історії потужності", + "service_status": "Стан обслуговування" }, "health": { "fair": "Прийнятна якість профілю", @@ -373,6 +386,10 @@ "poor": "⚠ Погана якість профілю" }, "lbl": { + "envelope_position_tip": "Позиція на власній записаній кривій зіставленої програми, {pct}%. Визначається суміщенням цього циклу з профілем, а не підрахунком часу, тому залишається правильною, коли запуск довший або коротший за звичайний. Оновлюється, доки прилад мовчить, тому під час активної фази може відставати.", + "envelope_position": "крива {pct}%", + "aria_playground_chart3": "Інтерактивний графік потужності циклу: наведіть, щоб побачити час/потужність, прокрутіть або використайте два пальці для масштабування, перетягніть для панорамування", + "device_unresolved": "Недоступний пристрій ({id})", "drag_to_resize": "Перетягніть, щоб змінити розмір", "actions": "Дії", "activity": "Активність", @@ -509,7 +526,7 @@ "profile_index": "індекс профілю →", "profile_name": "Назва профілю", "program": "Програма", - "program_tip": "Замініть профіль, що зіставляється з поточним циклом. Автоматичне виявлення дозволяє інтеграції автоматично вибрати найкраще співпадіння. Закріпіть конкретну програму, щоб примусово зіставити її, коли автоматичне виявлення неправильне або ви знаєте, що запущено.", + "program_tip": "Замініть профіль, що зіставляється з поточним циклом. Автоматичне виявлення дозволяє інтеграції автоматично вибрати найкраще співпадіння. Закріпіть конкретну програму, щоб примусово зіставити її, коли автоматичне виявлення неправильне або ви знаєте, що запущено. Виберіть програму до запуску приладу, і вона буде застосована, щойно почнеться наступний цикл.", "progress": "Прогрес", "raw_socket": "Необроблений сокет", "read_only": "Лише читання", @@ -544,6 +561,7 @@ "stage4": "Етап 4 – відповідність", "status": "Стан", "tail_trim": "Обрізання кінця (с)", + "tap_to_dismiss": "торкніться графіка, щоб закрити", "timer_auto_pause": "Автопауза", "timer_min": "хв", "timer_msg_placeholder": "Повідомлення (необов'язково, {device}/{program}/{minutes})", @@ -580,9 +598,7 @@ "pg_time_left_model": "Залишилось часу (модель)", "pg_time_left_model_tip": "Оцінка залишку часу моделлю (оцінювач фаз + ML), а не статичний відлік", "phase": "Фаза", - "pg_mode_simulate": "Симуляція", "pg_mode_history": "Перевірка на історії", - "pg_mode_sweep": "Перебір", "pg_ev_detected": "Виявлено", "pg_ev_match": "Збіг прийнято", "pg_ev_match_changed": "Збіг змінено", @@ -612,14 +628,11 @@ "pg_sweep_param": "Параметр", "pg_objective": "Ціль", "pg_steps": "Кроки", - "pg_sweep_2d": "2D теплова карта", - "pg_sweep_param_y": "2-й параметр", "pg_obj_match": "Точність збігів", "pg_obj_endtiming": "Точність завершення", "pg_obj_falseend": "Частота хибних завершень", "pg_obj_overrun": "Відхилення тривалості", "pg_obj_ambiguity": "Частота неоднозначності", - "pg_best_combo": "найкраща комбінація", "pg_mode_optimize": "Оптимізувати", "pg_group_matching": "Зіставлення програм", "pg_zoom_hint": "подвійний клік скидає масштаб", @@ -706,7 +719,12 @@ "task_history_import_apply": "Імпорт циклів", "evidence_real_cycles": "Цикли, виконані цим приладом", "evidence_reference_cycles": "Завантажені з магазину спільноти", - "evidence_backfill_cycles": "Знайдені в імпортованій історії потужності" + "evidence_backfill_cycles": "Знайдені в імпортованій історії потужності", + "zoom_hint_touch": "масштаб двома пальцями", + "zoom_hint_touch_zoomed": "масштаб двома пальцями · подвійний дотик для скидання", + "cycles_since_service": "{since} / {total} циклів", + "total_cycles_run": "циклів усього", + "correct_total_cycles": "Виправити загальну кількість" }, "log": { "all_levels": "Усі рівні", @@ -756,7 +774,7 @@ "merge_cycles": "Об'єднати {n} циклів", "new_group": "Нова група профілів", "new_phase": "Нова фаза", - "process_history_msg": "Повторно запустіть зіставлення, оновіть пропозиції, перенавчіть ML (якщо увімкнено) та перерахуйте якість циклів для всіх збережених циклів. Це може зайняти деякий час.", + "process_history_msg": "Повторно запустіть зіставлення, оновіть пропозиції, перенавчіть ML (якщо увімкнено), перерахуйте вартість за записаними цінами на енергію та перерахуйте якість циклів для всіх збережених циклів. Це може зайняти деякий час.", "process_history_ok": "Обробити", "process_history_title": "Обробка історії", "process_recording": "Обробити запис", @@ -789,6 +807,7 @@ "history_import": "Імпорт історії потужності" }, "msg": { + "pg_canvas_empty3": "Виберіть цикл вище й натисніть «Запустити», щоб змоделювати його. Потім наведіть, щоб побачити значення, прокрутіть або використайте два пальці для масштабування та перетягніть для панорамування.", "tail_trim_hint": "Скільки секунд обрізати з кінця", "store_sibling_hint": "Для вашої точної моделі нічого не опубліковано? Дуже схожа модель того самого бренду зазвичай є доброю відправною точкою.", "store_declare_appliance": "Вкажіть WashData, який у вас пристрій, і на цій вкладці з'являться конфігурації, опубліковані для нього іншими користувачами. Можна також ввести бренд вище і просто подивитися, що є.", @@ -802,7 +821,7 @@ "artifact_spike_detail": "Вийшов за верхню межу звичайної смуги потужності приблизно на ~{n} с.", "auto_label_intro": "Призначте профілі непозначеним циклам, чия впевненість збігу перевищує поріг.", "automations_intro": "WashData запускає події {start} / {end} та надає об'єкти, тому сповіщення й дії найкраще будувати як звичайні автоматизації Home Assistant. Автоматизації, що використовують цей пристрій, відображаються нижче.", - "cleanup_intro": "Усі позначені цикли накладено. Позначте аномалії і видаліть їх для очищення профілю.", + "cleanup_intro": "Усі позначені цикли накладено. Позначте аномалії, потім зніміть з них мітку (залишаться в історії, але більше не формуватимуть цей профіль) або видаліть їх остаточно.", "clear_debug_hint": "Видаліть збережені дані налагодження, щоб звільнити місце.", "collecting_data": "Збір даних. Залишилося циклів до початку точного налаштування: {need} ({current}/{min}).", "compare_overlay_profiles": "Накласти профілі (блідо)", @@ -902,7 +921,7 @@ "preferences_adv": "Налаштування", "prefs_personal": "Застосовується лише до вашого облікового запису Home Assistant.", "font_size_hint": "Зробіть усе на цій панелі більшим або меншим. Застосовується до вашого облікового запису на цьому пристрої.", - "process_history_hint": "Повторно запустіть зіставлення для всіх збережених циклів, оновіть пропозиції з налаштування, перенавчіть моделі ML (якщо увімкнено) та перерахуйте якість циклів. Запускайте після пакетного перегляду.", + "process_history_hint": "Повторно запустіть зіставлення для всіх збережених циклів, оновіть пропозиції з налаштування, перенавчіть моделі ML (якщо увімкнено), перерахуйте вартість циклів за записаними цінами на енергію та перерахуйте якість циклів. Запускайте це після серії переглядів.", "profile_deleted": "Профіль видалено", "profile_poor_health_detail": "Цикли, призначені цьому профілю, мають суперечливі форми або низьку надійність. Розгляньте можливість перебудови конверта або перегляду позначених циклів.", "profiles_intro": "Натисніть на профіль для статистики, фаз та очищення. Групуйте майже ідентичні програми (однакова форма/тривалість, різна температура або віджимання) для надійного вибору.", @@ -989,9 +1008,10 @@ "advisory_poor_health": "У '{name}' низька оцінка відповідності - його останні цикли сильно різняться або погано розпізнаються. Перегляньте його цикли або перезапишіть профіль, щоб розпізнавання та оцінки часу залишалися точними.", "advisory_shape_drift": "'{name}' суттєво відхилився від початкової форми кривої потужності. Можливо, поведінка приладу з часом змінилася (наприклад, накип, знос). Радимо перезаписати цей профіль за останніми циклами.", "advisory_shape_drift_corr": "'{name}' суттєво відхилився від початкової форми кривої потужності (кореляція {corr}). Можливо, поведінка приладу з часом змінилася (наприклад, накип, знос). Радимо перезаписати цей профіль за останніми циклами.", + "advisory_unmatchable": "'{name}' ніколи не буде зіставлений: за ним немає жодного циклу з даними потужності, тому WashData не може порівняти з ним поточний цикл. Позначте цикл цієї програми (або запишіть новий), щоб повернути профіль у зіставлення.", + "advisory_unmatchable_no_duration": "'{name}' ніколи не буде зіставлений: у нього є цикл із даними потужності, але без придатної тривалості, тому WashData не має еталонної довжини, за якою вимірює зіставлення. Запишіть цю програму ще раз або задайте її очікувану тривалість, щоб повернути профіль у зіставлення.", "pg_no_alerts": "У цьому запуску проблем не виявлено.", "pg_override_active": "Використовуються ваші змінені налаштування замість поточних", - "pg_history_intro": "Повторно прожене останні цикли через реальний детектор і зіставлювач. Спершу змініть налаштування виявлення в режимі «Симуляція», щоб побачити тут порівняння до/після.", "pg_history_empty": "Натисніть «Запустити», щоб відтворити цикли.", "pg_history_summary": "{detected}/{total} виявлено · {correct} зіставлено правильно", "pg_sweep_no_metric": "Недостатньо даних для оцінки цієї цілі.", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "Сканування не вдалося.", "hist_scan_expired": "Це сканування вже недоступне. Виконайте сканування ще раз.", "hist_import_failed": "Імпорт не вдався.", - "imported_history_readonly": "Виявлено в імпортованій історії потужності. Впливає на зіставлення програм, але не враховується у вашій статистиці; такий цикл не можна обрізати чи розділити. Натисніть «Позначити», щоб вказати програму." + "imported_history_readonly": "Виявлено в імпортованій історії потужності. Впливає на зіставлення програм, але не враховується у вашій статистиці; такий цикл не можна обрізати чи розділити. Натисніть «Позначити», щоб вказати програму.", + "odometer_intro": "Ця загальна кількість лише зростає. Видалення запису про цикл її не змінює, а підрахунок продовжується й поза межами ліміту збереженої історії, тому нагадування про обслуговування залишаються правильними.", + "odometer_correct_hint": "Задайте значення, якщо WashData записав цикл, якого не було, або якщо прилад уже виконував цикли до встановлення WashData.", + "no_reminders_set": "Нагадування про обслуговування ще не задано.", + "toast_program_armed": "Програму вибрано для наступного циклу: {program}" }, "phase_desc": { "anti_crease": "Час від часу короткі оберти після завершення для зменшення зморшок.", @@ -1161,6 +1185,10 @@ "doc": "Після того як цикл перевищує очікувану тривалість, скільки часу посудомийна машина має залишатися неактивною (нижче «Поріг зупинки»), перш ніж WashData припинить очікувати фінальний злив і завершить цикл. Збільште значення, якщо ваша машина має тривалу тиху фазу сушіння перед пізнім фінальним зливом, який пропускається - ширше вікно дозволяє вивченій тривалості слідувати за сезонними змінами (холодніша вхідна вода = довші цикли), а не фіксуватися на старому середньому значенні. Воно лише скорочує очікування відносно внутрішнього 30-хвилинного ліміту очікування завершального сплеску (відкачування води), але ніколи не збільшує його.", "label": "Тихе завершення пасивного сушіння" }, + "anti_crease_finalize_ratio": { + "doc": "Наскільки глибоко в очікувану тривалість зіставленої програми має зайти цикл, перш ніж WashData зможе визнати спостережуване обертання барабана на малій потужності хвостом захисту від зминання і завершити цикл на цьому місці. Очікувана тривалість - це середнє значення програми, тому на сушильній машині, час сушіння за датчиком вологості в якої залежить від завантаження, більшість запусків ніколи не досягає 98% цього значення, і хвіст тягнеться аж до резервного тайм-ауту - часто через 15-40 хвилин після того, як машина фактично зупинилася. На такій сушильній машині зменшіть значення (напр., 0.75). Для пральних машин залиште значення за замовчуванням: пральна машина проводить більшу частину циклу нижче налаштування Макс. потужність захисту від зминання, тож ця частка - єдине, що відокремлює тиху фазу середини прання від хвоста, і її зниження може скоротити прання удвічі. Залиште порожнім для значення за замовчуванням (0.98). Це не те саме, що Коефіцієнт розумного завершення, який керує окремою перевіркою дострокового завершення.", + "label": "Коефіцієнт завершення захисту від зминання" + }, "anti_wrinkle_enabled": { "doc": "Розпізнавайте короткі малопотужні імпульси обертання, які видає сушильна машина після основної фази нагрівання, і зберігайте їх прив’язаними до завершеного циклу замість того, щоб зчитувати їх як нові цикли.", "label": "Увімкнути виявлення захисту від зминання" @@ -1230,7 +1258,7 @@ "label": "Кількість повторів завершення" }, "energy_price_entity": { - "doc": "Датчик з поточною ціною електроенергії за кВт/год (наприклад, динамічний тариф). Має перевагу над статичною ціною нижче. Кожен цикл заморожує ціну, що діє після його завершення.", + "doc": "Сенсор із поточною ціною електроенергії за кВт·год (наприклад, динамічний тариф). Це має бути ціна, а не власна сутність потужності чи енергії розетки: вона має пріоритет над фіксованою ціною нижче, тож вказаний тут лічильник кВт·год рахує кожен цикл за показанням лічильника замість вашого тарифу. Коли увімкнено «Вартість, зважена за часом», кожен цикл обчислюється за ціною, що діяла в кожну мить його роботи; інакше за ним закріплюється ціна, що діяла на момент завершення.", "label": "Сутність ціни на енергію" }, "energy_price_static": { @@ -1258,7 +1286,7 @@ "label": "Інвертувати зовнішній тригер (спрацювання при ВИМКНЕННІ)" }, "learning_confidence": { - "doc": "Якщо показник збігу знаходиться між цим значенням і впевненістю автоматичної мітки, у сповіщенні відгуку буде запропоновано підтвердити ідентифіковану програму. Нижче цього рахунку матч є надто непевним, щоб вийти на поверхню. Має бути нижче рівня впевненості автоматичної мітки.", + "doc": "Якщо показник відповідності перебуває між цим значенням і Впевненістю автоматичного маркування, сповіщення зворотного зв’язку попросить вас підтвердити розпізнану програму. Нижче цього показника відповідність надто непевна, щоб її показувати. Має залишатися нижчою за Впевненість автоматичного маркування.", "label": "Впевненість навчання" }, "linked_device": { @@ -1326,7 +1354,7 @@ "label": "Запускати події HA для сповіщень" }, "notify_icon": { - "doc": "Додаткова піктограма mdi для сповіщення (наприклад, mdi:washing-machine). Залиште порожнім для платформи за замовчуванням.", + "doc": "Невелика піктограма, що показується у сповіщенні (наприклад, mdi:washing-machine). Android малює її в рядку стану; iOS показує її замість піктограми застосунку, і для цього потрібен супутній застосунок 2026.8 або новіший. Залиште порожнім, щоб використовувати піктограму за замовчуванням для платформи.", "label": "Іконка сповіщення" }, "notify_live_chronometer": { @@ -1426,7 +1454,7 @@ "label": "Поріг зіставлення" }, "profile_unmatch_threshold": { - "doc": "If a live mid-cycle match drops below this score, the tentative identification is cleared. Тримайте його трохи нижче порога відповідності, щоб короткочасне зниження подібності не повернуло дисплей назад до незрівнянного.", + "doc": "Якщо відповідність у реальному часі в середині циклу опускається нижче цього показника, попередня ідентифікація скасовується. Тримайте його трохи нижче Порога зіставлення, щоб короткочасне зниження подібності не повернуло відображення до стану без відповідності.", "label": "Поріг незіставлення" }, "progress_reset_delay": { @@ -1437,10 +1465,6 @@ "doc": "Через кілька секунд насос може працювати безперервно, перш ніж його буде позначено як можливе зависання (спрацьовує подія stuck-pump).", "label": "Тривалість застрявання насоса" }, - "running_dead_zone": { - "doc": "Після початку циклу спади потужності в цьому вікні ігноруються. Пральні машини заповнюють холодною водою (втрачаючи близько 0 Вт перед нагріванням) - без цього захисту ця фаза заповнення виглядає як кінець циклу. Це НЕ пропускає дані: повна потужність записується з T=0. Механізм підказок вимірює фактичний шаблон запуску вашого комп’ютера та автоматично визначає його розмір.", - "label": "Мертва зона під час роботи" - }, "sampling_interval": { "doc": "Очікуваний час між показаннями датчика – використовується для визначення розміру вікна згладжування та правильного початку усунення дребезгу. Кожне оновлення датчика реєструється незалежно від цього значення; він лише калібрує наступні обчислення. Механізм підказок вимірює фактичну частоту педалей вашого датчика з минулих циклів і встановлює її автоматично.", "label": "Інтервал вибірки" @@ -1568,13 +1592,17 @@ "label": "Інтервал профілю потужності", "doc": "Розмір сегмента для атрибута датчика power_profile по профілю (плоский масив середніх ват по слотах), що використовується зовнішніми планувальниками, такими як EMHASS та tibber_prices. Менші сегменти зберігають короткі сплески потужності гострими; більші сегменти згладжують форму. За замовчуванням 15 хв. Лише для читання; не впливає на виявлення." }, + "notify_live_silent": { + "label": "Тихі живі оновлення", + "doc": "Лише для iOS. Оновлювати хід виконання тихо: кожне оновлення надходить без звуку та вібрації, зі зниженим пріоритетом для економії заряду, тож iOS може ненадовго групувати їх. Перше оновлення циклу (те, що запускає Live Activity), а також сповіщення про початок і завершення сповіщають як зазвичай. Вимкніть, щоб отримувати сигнал на кожне оновлення." + }, "notify_live_sticky": { "label": "Зберігати живе сповіщення при натисканні", "doc": "Лише для Android. Зробіть сповіщення про хід виконання постійним (закріпленим), щоб натискання не закривало його. Вимкнено зберігає поведінку за замовчуванням, де натискання закриває сповіщення." }, "notify_live_click_action": { - "label": "Ціль натискання живого сповіщення", - "doc": "Лише для Android. Куди відкривається натискання на сповіщення про хід виконання (наприклад, /lovelace/laundry або повний URL), замість початкової сторінки застосунку. Залиште порожнім для поведінки за замовчуванням." + "label": "Ціль натискання сповіщення", + "doc": "Куди відкривається натискання на сповіщення від цього приладу, на Android та iOS. Порожнє поле відкриває панель WashData на цьому приладі. Укажіть шлях до інформаційної панелі (наприклад, /lovelace/laundry) або повний URL, щоб перейти в інше місце, або «none», щоб натискання нічого не відкривало." }, "door_opens_at_end": { "label": "Дверцята відкриваються автоматично наприкінці", @@ -1587,6 +1615,14 @@ "profile_evidence_sources": { "label": "Цикли, що формують програму", "doc": "Які цикли беруть участь у побудові кривої потужності кожної програми та в зіставленні з нею завершеного циклу. Якщо зняти позначку з певного виду, він більше не формує ваші програми, але нічого не видаляється: цикли залишаються у списку Цикли, їх так само можна позначити або вилучити. Корисно, якщо ви не довіряєте імпортованим даним. На статистику це не впливає: у ній завжди враховуються лише цикли, які цей прилад справді виконав. Якщо зняти всі позначки, це буде проігноровано, адже програма без жодного циклу ніколи не могла б зіставитися." + }, + "energy_price_dynamic": { + "label": "Вартість, зважена за часом", + "doc": "Обчислює вартість кожного циклу за ціною, що діяла в кожну мить його роботи, а не за єдиною ціною на момент завершення. Працює лише із сутністю ціни (фіксована ціна змінюватися не може). Додаткового налаштування не потрібно: поки цикл триває, ціна відстежується наживо, а Обробка історії в Діагностиці перераховує вартість давніших циклів за даними recorder. Вимкнено = класична поведінка, одна ціна, зафіксована наприкінці." + }, + "curve_preroll_seconds": { + "doc": "Скільки секунд показань від перерваних спроб запуску WashData може перенести на початок кривої циклу. Крива циклу зазвичай починається зі спроби запуску, яка врешті вдалася, тому на машині, яка робить кілька проб, перш ніж вийти на режим - вибір програми, блокування дверцят, перший набір води, - у кожній кривій бракує перших хвилин справжньої роботи. Задайте значення трохи більше за найдовший такий розгін вашої машини (300 с із запасом вистачає більшості). 0 вимикає цю функцію. Зважте, що її увімкнення зсуває записаний початок раніше, тому цикли, записані до зміни та після неї, мають різну тривалість для однієї й тієї самої програми; вивчені середні будуть зміщуватися, доки старіші цикли не вийдуть з історії.", + "label": "Передзапис кривої" } }, "setting_group": { @@ -1698,7 +1734,8 @@ "splitting": "Розділення…", "deleting": "Видалення…", "imported": "Імпортовано", - "preparing": "Підготовка…" + "preparing": "Підготовка…", + "unlabelling": "Зняття мітки…" }, "tab": { "advanced": "Розширені", @@ -1778,6 +1815,7 @@ "recording_stopped": "Запис зупинено", "review_saved": "Огляд збережено", "settings_conflicts": "Виправте виділені конфлікти налаштувань перед збереженням.", + "settings_no_changes": "Немає змін для збереження", "settings_reverted": "Налаштування відновлено; інтеграція перезавантажується", "settings_saved": "Налаштування збережено; інтеграція перезавантажується", "suggestions_applied": "Пропозиції застосовано; інтеграція перезавантажується", @@ -1836,7 +1874,11 @@ "export_selective_done": "Експорт завантажено", "import_selective_done": "Імпортовано профілів: {profiles}, циклів: {cycles}", "hist_csv_required": "Спочатку завантажте файл CSV або вставте його вміст", - "file_read_failed": "Не вдалося прочитати цей файл" + "file_read_failed": "Не вдалося прочитати цей файл", + "unlabel_done": "Знято мітку з циклів: {count}", + "unlabel_failed": "Не вдалося зняти мітку: {error}", + "odometer_saved": "Загальну кількість циклів збережено", + "odometer_save_failed": "Не вдалося зберегти загальну кількість: {error}" }, "suggestion": { "both_agree": "WashData рекомендує", @@ -1883,10 +1925,6 @@ "higher": "Підтверджує лише збіги з високою впевненістю; більше циклів залишаються без мітки", "lower": "Зіставляє більше циклів; деякі можуть бути дещо неправильними" }, - "running_dead_zone": { - "higher": "Ігнорує короткочасні стрибки потужності в режимі простою – менш чутливий до шуму", - "lower": "Реагує на коротші сплески потужності – вловлює швидку перехідну активність" - }, "start_threshold_w": { "higher": "Запобігає хибним запускам від короткочасних стрибків потужності", "lower": "Вловлює маломощні програми; може спрацювати на шум" @@ -1911,6 +1949,8 @@ "end_repeat_count": "{false}/{total} коректних циклів ({pct}%) мали внутрішню паузу >60s з відновленням; потрібно {val} підтвердження завершення.", "learning_confidence": "Впевненість p05 для {cycles} циклів, розмічених користувачем ({p05}); нижче цього значення запитується перевірка.", "match_interval": "На основі спостережуваної частоти оновлень (медіана={median}s) * 10.", + "match_interval_capped": "Обмежено так, щоб {persistence} послідовних зіставлень вмістилися в {pct}% найкоротшої програми ({shortest}s); сама лише частота оновлень (медіана={median}s) підказала б довший інтервал.", + "match_interval_floored": "Уже сама лише найкоротша програма ({shortest}s) обмежила б це значення до {cap}s, що нижче за мінімум {minimum}s, тому воно втримується на мінімумі: серія з {persistence} послідовних зіставлень триває {budget}s, тобто більше ніж {pct}% цієї програми. Зіставлення виконується лише тоді, коли надходить нове показання (медіана={median}s), тому мінімум не додає навантаження.", "max_duration_ratio": "На основі тривалостей розмічених циклів (p95={p95}).", "min_active": "На основі мінімальної активної потужності ({min}W), зафіксованої в останньому циклі.", "min_duration_ratio": "Залишено якомога нижчим, щоб програма розпізнавалася на початку циклу; пороги впевненості та неоднозначності запобігають передчасним підтвердженням.", @@ -1973,6 +2013,7 @@ "start_duration_threshold": "Секунди вище порогу для підтвердження старту", "start_threshold_w": "Мінімум ват, щоб вважати запущеним", "stop_threshold_w": "Нижче цього машина вважається вимкненою", + "anti_crease_finalize_ratio": "Частка очікуваної тривалості зіставленої програми, якої має досягти цикл, перш ніж можна завершити хвіст обертання захисту від зминання; зменшіть її на сушильних машинах із довгим хвостом обертання, для пральних машин залиште значення за замовчуванням", "anti_wrinkle_enabled": "Поглинати імпульси перекидання барабана після основної фази, а не зчитувати їх як нові цикли", "anti_wrinkle_exit_power": "Потужність між імпульсами має падати нижче цього, щоб захист від зминання залишався активним", "anti_wrinkle_idle_timeout": "Дозволена тиша між двома імпульсами перекидання барабана, перш ніж захист від зминання завершиться", @@ -1990,7 +2031,8 @@ "dtw_ddtw_scale": "Етап 3: відстань напівнасичення DDTW; менша = більш чутлива до форми (за замовчуванням 30)", "dtw_refine_top_n": "Етап 3: кандидати, які DTW переоцінює; збільшіть до 7-9, якщо правильний профіль займає 4-5 місце (за замовчуванням 5)", "duration_scale": "Етап 4: лог-відношення, при якому збіг за тривалістю зменшується вдвічі; менше = суворіший штраф (за замовчуванням 0,175)", - "energy_scale": "Етап 4: лог-відношення, при якому збіг за енергією зменшується вдвічі; менше = суворіший штраф (за замовчуванням 0,25)" + "energy_scale": "Етап 4: лог-відношення, при якому збіг за енергією зменшується вдвічі; менше = суворіший штраф (за замовчуванням 0,25)", + "curve_preroll_seconds": "Секунди показань від перерваних спроб запуску, що переносяться на початок кривої циклу; 0 = вимкнено" }, "col": { "profile_tip": "Назва зіставленої програми. «Без мітки» означає, що наприкінці циклу не збігся жоден профіль.", @@ -1998,10 +2040,11 @@ "date_tip": "Дата та час початку циклу.", "duration_tip": "Загальний час роботи циклу від початку до кінця.", "energy_tip": "Загальна спожита енергія (кВт·год). Обчислюється інтегруванням потужності за часом.", - "cost_tip": "Вартість енергії для цього циклу, зафіксована при завершенні за чинною тоді ціною (енергія × ціна за кВт·год). Укажіть ціну в Налаштуваннях, щоб заповнити це поле.", + "cost_tip": "Вартість енергії для цього циклу, зафіксована при завершенні. Коли увімкнено «Вартість, зважена за часом», крива потужності обчислюється за ціною, що діє в кожну мить; інакше енергія x єдина ціна, що діє наприкінці. Задайте ціну в Налаштуваннях, щоб значення заповнилося.", "confidence_tip": "Впевненість збігу профілю (0–100%). Наскільки точно крива потужності циклу збіглася з визначеною програмою.", "health_tip": "ML-стан циклу (вище = краще). Натисніть на цикл, щоб перевірити та переглянути його.", - "flags_tip": "Мітки спільноти та індикатори якості даних" + "flags_tip": "Мітки спільноти та індикатори якості даних", + "cost_dynamic_tip": "Зважено за часом: обчислено за ціною, що діяла протягом циклу (ефективно {price} {cur}/кВт·год)." }, "stat": { "avg": "сер.", @@ -2028,9 +2071,9 @@ "finished": "Цикл досяг кінцевого стану й завершився." }, "store": { + "search_ph2": "Пошук за брендом або моделлю…", "your_model_tip": "Це пристрій, який ви вказали в налаштуваннях", "your_model": "Ваш", - "search_brand_ph": "Пошук за брендом…", "programs_count": "Програми: {n}", "browse": "Огляд", "device": "Пристрій", @@ -2075,7 +2118,8 @@ "golden": "Повторна обробка: заповнення контрольних", "suggestions": "Повторна обробка: пропозиції", "ml_training": "Повторна обробка: ML-навчання", - "health": "Повторна обробка: стан циклів" + "health": "Повторна обробка: стан циклів", + "costs": "Повторна обробка: витрати на енергію" }, "pg_sweep": { "optimize": "Оптимізація: {param}" diff --git a/custom_components/ha_washdata/translations/panel/zh-Hans.json b/custom_components/ha_washdata/translations/panel/zh-Hans.json index 23b9ab0d..74df26b1 100644 --- a/custom_components/ha_washdata/translations/panel/zh-Hans.json +++ b/custom_components/ha_washdata/translations/panel/zh-Hans.json @@ -48,6 +48,8 @@ } }, "badge": { + "quiet_tail_tip": "接近结束时,该程序会静默约 {mins} 分钟,然后在结束前以约 {watts} W 运行约 {secs} 秒。在已测量的 {measured} 个周期中有 {seen} 个出现过:设备并非每次运行都会如此。", + "quiet_tail": "结尾静默约 {mins} 分钟", "artifact_tip": "检测到 {n} 个异常(例如,门在周期中打开)– 打开以在图表上查看", "auto": "(自动检测)", "built_in_tag": "内置", @@ -77,11 +79,14 @@ "approved": "已批准", "awaiting": "等待批准", "imported_tip": "从社区商店导入。仅用于匹配,不计入统计。", + "unmatchable": "无法匹配", "not_importable": "此处不适用", "exists": "已存在", - "backfilled_tip": "在导入的功率历史中检测到。仅影响程序匹配,不计入统计。" + "backfilled_tip": "在导入的功率历史中检测到。仅影响程序匹配,不计入统计。", + "armed": "(应用于下一个周期)" }, "btn": { + "reset_zoom": "重置缩放", "set_brand_model": "设置品牌和型号", "refresh_catalog": "刷新目录", "add_device": "+ 添加设备", @@ -215,6 +220,8 @@ "trim": "裁剪", "trim_split": "裁剪 / 拆分", "undo": "撤销", + "unlabel_selected": "取消标记所选({n})", + "unlabel_selected_tip": "从所选周期中移除配置文件标签。这些周期仍留在您的历史中,之后可以重新标记,只是不再用于构建此配置文件。", "use": "使用", "wipe_all": "清除所有数据", "delete_selected": "删除所选({n})", @@ -222,7 +229,6 @@ "pg_apply_best": "应用最佳值", "pg_apply_to_settings": "保存到设置", "pg_apply_to_settings_tip": "将您在此处编辑的值复制到该设备当前生效的设置中", - "search": "搜索", "import": "导入", "disconnect": "断开连接", "connect_github": "连接 GitHub", @@ -249,7 +255,8 @@ "hist_read_recorder": "从 Home Assistant 读取", "hist_scan": "扫描周期", "hist_import_n": "导入 {n} 个周期", - "hist_goto_cycles": "查看这些周期" + "hist_goto_cycles": "查看这些周期", + "save_total_cycles": "保存累计值" }, "conflict": { "anti_wrinkle_exit": { @@ -292,6 +299,12 @@ "stop": "必须高于电源关闭阈值 ({pot} W)", "threshold": "必须低于停止阈值 ({stop} W) 才能生效" }, + "price_entity": { + "power_sensor": "这是该设备的功率传感器,不是电价。价格实体会覆盖下面的固定电价,这样每个周期都会把瓦数当作每 kWh 单价来计费,请清空此字段。", + "energy_sensor": "这是该设备的电能表,不是电价。价格实体会覆盖下面的固定电价,这样每个周期都会把电表读数当作每 kWh 单价来计费,请清空此字段。", + "device_class": "该传感器测量的是 {dc},不是每 kWh 电价。价格实体会覆盖下面的固定电价,请清空此字段,或改为指向电价(费率)传感器。", + "unit": "该传感器的读数单位是 {unit},不是每 kWh 电价。价格实体会覆盖下面的固定电价,请清空此字段,或改为指向电价(费率)传感器。" + }, "pump_stuck": { "duration": "必须小于无更新超时 ({to} 秒),以确保卡泵警报在看门狗终止周期前触发", "timeout": "必须大于泵卡住时长 ({ps} 秒),以确保卡泵警报在周期被强制停止前触发" @@ -360,12 +373,12 @@ "wipe_history": "清除历史", "recommendations": "建议({n})", "pg_detection_params": "检测设置", - "pg_timeline": "事件时间线", "pg_outcome": "模拟结果", "pg_across_cycles": "所有周期综合", "community_store": "社区商店", "online_account": "社区商店与在线功能", - "import_power_history": "导入功率历史" + "import_power_history": "导入功率历史", + "service_status": "保养状态" }, "health": { "fair": "配置文件质量尚可", @@ -373,6 +386,10 @@ "poor": "⚠ 配置文件质量差" }, "lbl": { + "envelope_position_tip": "在匹配程序自身记录曲线上的位置,{pct}%。它通过将本次周期与配置文件对齐来测量,而不是计算时间,因此运行比平时更长或更短时仍然准确。它在设备静默时刷新,因此在活跃阶段可能会滞后。", + "envelope_position": "曲线 {pct}%", + "aria_playground_chart3": "交互式周期功率图:悬停查看时间/功率,滚动或双指缩放,拖动平移", + "device_unresolved": "不可用的设备 ({id})", "drag_to_resize": "拖动以调整大小", "actions": "操作", "activity": "活动", @@ -508,7 +525,7 @@ "profile_index": "配置文件序号 →", "profile_name": "配置文件名称", "program": "程序", - "program_tip": "覆盖与当前周期匹配的配置文件。自动检测让集成自动选择最佳匹配。当自动检测出错或您知道正在运行什么时,固定特定程序以强制匹配。", + "program_tip": "覆盖与当前周期匹配的配置文件。自动检测让集成自动选择最佳匹配。当自动检测出错或您知道正在运行什么时,固定特定程序以强制匹配。在启动家电之前选择一个程序,下一个周期开始时即会应用。", "progress": "进度", "raw_socket": "原始套接字", "read_only": "只读", @@ -543,6 +560,7 @@ "stage4": "阶段 4 – 一致度", "status": "状态", "tail_trim": "末尾裁剪(秒)", + "tap_to_dismiss": "点按图表关闭", "timer_auto_pause": "自动暂停", "timer_min": "分钟", "timer_msg_placeholder": "消息(可选,{device}/{program}/{minutes})", @@ -579,9 +597,7 @@ "pg_time_left_model": "剩余时间(模型)", "pg_time_left_model_tip": "由模型估算的剩余时间(阶段估算 + ML 混合),并非固定倒计时。", "phase": "阶段", - "pg_mode_simulate": "模拟", "pg_mode_history": "历史测试", - "pg_mode_sweep": "参数扫描", "pg_ev_detected": "已检测", "pg_ev_match": "匹配确定", "pg_ev_match_changed": "匹配已更改", @@ -617,14 +633,11 @@ "pg_stress_idle_w_desc": "覆盖自动检测的待机功率下限(留空则使用自动值)。", "pg_stress_toggle": "测试闲置终止", "pg_stress_toggle_desc": "模拟设备在录制结束后维持待机功耗水平,查看 WashData 是否以及何时停止运行周期。", - "pg_sweep_2d": "2D 热图", - "pg_sweep_param_y": "第二个参数", "pg_obj_match": "匹配准确度", "pg_obj_endtiming": "结束时机准确度", "pg_obj_falseend": "误结束率", "pg_obj_overrun": "时长偏离目标", "pg_obj_ambiguity": "不明确率", - "pg_best_combo": "最佳组合", "pg_mode_optimize": "优化", "pg_group_matching": "程序匹配", "pg_zoom_hint": "双击可重置缩放", @@ -706,7 +719,12 @@ "task_history_import_apply": "导入周期", "evidence_real_cycles": "本机实际运行的周期", "evidence_reference_cycles": "从社区商店下载", - "evidence_backfill_cycles": "在导入的功率历史中发现" + "evidence_backfill_cycles": "在导入的功率历史中发现", + "zoom_hint_touch": "双指缩放", + "zoom_hint_touch_zoomed": "双指缩放 · 双击重置", + "cycles_since_service": "{since} / {total} 个周期", + "total_cycles_run": "累计运行周期", + "correct_total_cycles": "更正累计值" }, "log": { "all_levels": "所有级别", @@ -756,7 +774,7 @@ "merge_cycles": "合并 {n} 个周期", "new_group": "新建配置文件组", "new_phase": "新建阶段", - "process_history_msg": "重新运行匹配、刷新建议、重新训练 ML(如果启用)并重新计算所有存储循环的循环健康状态。这可能需要一些时间。", + "process_history_msg": "对所有存储的周期重新运行匹配、刷新建议、重新训练 ML(如果启用)、按记录的电价重新计算各周期的费用,并重新计算周期健康。这可能需要一些时间。", "process_history_ok": "处理", "process_history_title": "处理历史记录", "process_recording": "处理录制", @@ -789,6 +807,7 @@ "history_import": "导入功率历史" }, "msg": { + "pg_canvas_empty3": "在上方选择一个周期并按“运行”进行模拟。然后悬停查看数值,滚动或双指缩放,拖动平移。", "tail_trim_hint": "从末尾移除的秒数", "store_sibling_hint": "没有与您型号完全一致的分享内容?同品牌中相近的型号通常是不错的起点。", "store_declare_appliance": "告知 WashData 您拥有哪台家电,此标签页便会显示其他用户为它分享的程序和录制。您也可以在上方输入品牌名称随意浏览。", @@ -802,7 +821,7 @@ "artifact_spike_detail": "超出通常功率范围约 {n} 秒。", "auto_label_intro": "为置信度达到阈值的未标记周期分配配置文件。", "automations_intro": "WashData 触发 {start} / {end} 事件并公开实体,因此通知和操作最好构建为常规 Home Assistant 自动化。使用此设备的自动化显示在下方。", - "cleanup_intro": "显示所有已标记周期的叠加。勾选离群值并删除以清理配置文件。", + "cleanup_intro": "显示所有已标记周期的叠加。勾选离群值,然后取消其标记(仍留在历史中,但不再用于构建此配置文件)或将其彻底删除。", "clear_debug_hint": "删除存储的调试数据以释放空间。", "collecting_data": "正在收集数据。开始微调前还需要的周期数:{need}({current}/{min})。", "compare_overlay_profiles": "叠加配置文件(浅色)", @@ -901,7 +920,7 @@ "preferences_admin": "偏好、面板和访问控制", "preferences_adv": "偏好", "prefs_personal": "这些仅适用于您的 Home Assistant 账户。", - "process_history_hint": "对所有存储的循环重新运行匹配、刷新调优建议、重新训练 ML 模型(如果启用)并重新计算循环健康状态。在批量审查后运行此项。", + "process_history_hint": "对所有存储的周期重新运行匹配、刷新调优建议、重新训练 ML 模型(如果启用)、按记录的电价重新计算各周期的费用,并重新计算周期健康。在批量审查后运行此项。", "profile_deleted": "配置文件已删除", "profile_poor_health_detail": "分配给此配置文件的周期形状不一致或置信度较低。考虑重建包络或审查已标记的周期。", "profiles_intro": "点击配置文件查看统计、阶段和清理。将形状/时长相近(不同温度或转速)的程序分组,以便匹配可靠地区分它们。", @@ -984,9 +1003,10 @@ "advisory_poor_health": "'{name}' 的拟合度评分较低 - 其近期周期波动较大或匹配较弱。请检查其周期或重新录制该配置文件,以保持匹配和时间估算的准确性。", "advisory_shape_drift": "'{name}' 已明显偏离其原始功率曲线。设备行为可能随时间发生了变化(例如水垢、磨损)。建议用近期周期重新录制此配置文件。", "advisory_shape_drift_corr": "'{name}' 已明显偏离其原始功率曲线(相关系数 {corr})。设备行为可能随时间发生了变化(例如水垢、磨损)。建议用近期周期重新录制此配置文件。", + "advisory_unmatchable": "'{name}' 永远无法被匹配:其背后没有任何带功率数据的周期,因此 WashData 没有可与运行中的周期进行比较的对象。请为此程序标记一个周期(或录制一个周期),使其重新参与匹配。", + "advisory_unmatchable_no_duration": "'{name}' 永远无法被匹配:它有带功率数据的周期,但没有可用的时长,因此 WashData 没有可用来衡量匹配的长度。请重新录制此程序,或设置其预期时长,使其重新参与匹配。", "pg_no_alerts": "本次运行未检测到问题。", "pg_override_active": "正在使用您编辑的设置对比当前设置", - "pg_history_intro": "用真实的检测器和匹配器重放您最近的周期。若要在此查看前后对比,请先在模拟中编辑一项检测设置。", "pg_history_empty": "按运行以重放您的周期。", "pg_history_summary": "{detected}/{total} 已检测 · {correct} 正确匹配", "pg_sweep_no_metric": "数据不足,无法为此目标评分。", @@ -1063,7 +1083,11 @@ "hist_scan_failed": "扫描失败。", "hist_scan_expired": "该次扫描结果已不可用,请重新扫描。", "hist_import_failed": "导入失败。", - "imported_history_readonly": "在导入的功率历史中检测到。它会影响程序匹配,但不计入您的统计,且无法裁剪或拆分。请标记以指定其程序。" + "imported_history_readonly": "在导入的功率历史中检测到。它会影响程序匹配,但不计入您的统计,且无法裁剪或拆分。请标记以指定其程序。", + "odometer_intro": "此累计值只会增加。删除某条周期记录不会改变它,并且它会在保存的历史记录上限之外继续累加,因此保养提醒始终准确。", + "odometer_correct_hint": "如果 WashData 记录了从未发生的运行,或者设备在安装 WashData 之前已经运行过周期,请设置此值。", + "no_reminders_set": "尚未设置保养提醒。", + "toast_program_armed": "已为下一个周期设置程序:{program}" }, "pg_desc": { "completion_min_seconds": "计为真实周期的最短运行时间", @@ -1093,7 +1117,9 @@ "duration_scale": "Stage 4: 时长一致性得分减半时的对数比率; 越小惩罚越严格(默认 0.175)", "energy_scale": "Stage 4: 能耗一致性得分减半时的对数比率; 越小惩罚越严格(默认 0.25)", "dishwasher_end_spike_quiet_release": "洗碗机:预期时长过后、解除周期结束排水等待之前保持静止的秒数", - "smart_termination_duration_ratio": "匹配程序预期时长中,周期在智能终止可以提前结束之前必须达到的比例;对于取决于负载或温度的机器请调低" + "smart_termination_duration_ratio": "匹配程序预期时长中,周期在智能终止可以提前结束之前必须达到的比例;对于取决于负载或温度的机器请调低", + "anti_crease_finalize_ratio": "在可以结束防皱翻滚尾段之前,周期必须达到的匹配程序预期时长的比例;翻滚尾段较长的烘干机请调低,洗衣机保持默认值", + "curve_preroll_seconds": "并入周期曲线开头的中止启动尝试读数秒数;0 = 关闭" }, "phase_desc": { "anti_crease": "完成后偶尔进行短暂的翻滚以减少皱纹。", @@ -1208,7 +1234,7 @@ "label": "最大防皱功率" }, "auto_label_confidence": { - "doc": "如果周期结束时的比赛分数等于或高于此分数,则节目将自动标记,无需任何确认提示。在自动标记之前将其提高到需要更高的确定性;降低它以实现更多自动化。与下面的学习信心结合使用。", + "doc": "如果周期结束时的匹配分数达到或高于此值,程序将被自动标记,无需任何确认提示。提高它可在自动标记前要求更高的确定性;降低它可实现更多自动化。与下方的学习置信度配合使用。", "label": "自动标记置信度" }, "auto_maintenance": { @@ -1256,7 +1282,7 @@ "label": "结束重复次数" }, "energy_price_entity": { - "doc": "具有当前每千瓦时电价的传感器(例如动态电价)。优先于下面的静态价格。每个周期结束后都会冻结实际价格。", + "doc": "提供当前每 kWh 电价的传感器(例如动态电价)。必须是电价,不能选插座自身的功率或电能实体:它优先于下面的固定价格,因此这里若填 kWh 累计电能计数器,每个周期都会按电表读数而不是你的电价来计费。启用“时间加权成本”后,每个周期按其运行期间每一时刻生效的电价计费;否则按结束时生效的电价冻结。", "label": "电价实体" }, "energy_price_static": { @@ -1284,7 +1310,7 @@ "label": "反转外部触发器(关闭时触发)" }, "learning_confidence": { - "doc": "如果匹配分数介于此分数和自动标签置信度之间,则会出现反馈通知,要求您验证已识别的程序。低于这个分数,比赛就太不确定了,无法浮出水面。必须保持在自动标签置信度以下。", + "doc": "如果匹配分数介于此值和自动标记置信度之间,则会发出反馈通知,请您验证已识别的程序。低于此分数时,匹配过于不确定,不会显示。必须保持在自动标记置信度以下。", "label": "学习置信度" }, "linked_device": { @@ -1352,7 +1378,7 @@ "label": "为通知触发 HA 事件" }, "notify_icon": { - "doc": "通知的可选 mdi 图标(例如 mdi:洗衣机)。将平台默认值留空。", + "doc": "通知上显示的小图标(例如 mdi:washing-machine)。Android 会将其绘制在状态栏中;iOS 则用它代替应用图标,需要配套应用 2026.8 或更高版本。留空则使用平台默认值。", "label": "通知图标" }, "notify_live_chronometer": { @@ -1452,7 +1478,7 @@ "label": "匹配阈值" }, "profile_unmatch_threshold": { - "doc": "如果实时中期比赛的分数低于此分数,则临时标识将被清除。将其保持在匹配阈值以下一点,这样相似度的短暂下降不会将显示翻转回不匹配的状态。", + "doc": "如果周期运行期间的实时匹配分数低于此分数,则临时识别结果将被清除。请将其保持在匹配阈值稍下方,这样相似度的短暂下降不会使显示退回到未匹配状态。", "label": "不匹配阈值" }, "progress_reset_delay": { @@ -1463,10 +1489,6 @@ "doc": "泵在被标记为可能卡住之前可以连续运行的秒数(触发泵卡住事件)。", "label": "泵卡住时长" }, - "running_dead_zone": { - "doc": "一个周期开始后,该窗口内的功率骤降将被忽略。洗衣机注满冷水(加热前下降到接近 0 W) - 如果没有这种保护,注水阶段看起来就像循环结束。这不会跳过数据:从 T=0 开始记录全功率轨迹。建议引擎会测量机器的实际启动模式并自动调整其大小。", - "label": "运行死区" - }, "sampling_interval": { "doc": "传感器读数之间的预期时间 - 用于调整平滑窗口的大小并正确启动去抖。无论该值如何,都会捕获每个传感器更新;它仅校准下游计算。建议引擎会测量传感器过去周期的实际踏频并自动设置。", "label": "采样间隔" @@ -1594,13 +1616,17 @@ "label": "电力分布间隔", "doc": "外部规划工具(如 EMHASS 和 tibber_prices)使用的 per-profile power_profile 传感器属性(每槽平均瓦数的数组)的桶大小。较小的桶保留短暂功率尖峰的细节,较大的桶平滑形状。默认 15 分钟。仅在读取时使用,不影响检测。" }, + "notify_live_silent": { + "label": "静默实时更新", + "doc": "仅限 iOS。安静地刷新实时进度:每次更新都不会发出声音或震动,并以更省电的较低优先级送达,iOS 可能会将其短暂合并后再显示。每个周期的第一次更新(即启动实时活动的那次)以及开始/完成通知仍会照常提醒。关闭此选项可在每次更新时都收到提醒。" + }, "notify_live_sticky": { "label": "点击后保留实时通知", "doc": "仅限 Android。将实时进度通知设置为固定(常驻),点击时不会关闭正在进行的通知线程。关闭则恢复默认行为,点击后通知消失。" }, "notify_live_click_action": { - "label": "实时通知点击目标", - "doc": "仅限 Android。点击实时进度通知时打开的位置(例如 /lovelace/laundry 或完整 URL),而非应用默认首页。留空使用默认值。" + "label": "通知点击目标", + "doc": "点击此家电的通知时打开的位置,在 Android 和 iOS 上均适用。留空则打开显示此家电的 WashData 面板。若要跳转到其他位置,请输入仪表板路径(例如 /lovelace/laundry)或完整 URL;若不需要任何点击目标,请输入“none”。" }, "door_opens_at_end": { "label": "结束时自动开门", @@ -1617,6 +1643,18 @@ "profile_evidence_sources": { "label": "用于构建程序的周期", "doc": "用于构建每个程序的功率曲线,并将已完成的周期与其匹配的周期种类。取消勾选某一种类会使其不再影响您的程序,但不会删除任何内容 - 这些周期仍留在周期列表中,仍可标记或删除。如果您不信任导入的数据,此选项很有用。统计不受影响:统计始终只计入本机实际运行过的周期。取消勾选全部会被忽略,因为没有任何周期作为依据的程序永远无法匹配。" + }, + "energy_price_dynamic": { + "label": "时间加权成本", + "doc": "按周期运行期间每一时刻生效的电价计费,而不是只用结束时的单一价格。仅对价格实体生效(固定价格不会变动)。无需额外设置:周期运行时会实时跟踪电价,诊断下的“处理历史”会根据 recorder 的记录重新计算旧周期的费用。关闭 = 传统方式,按结束时的单一价格冻结。" + }, + "anti_crease_finalize_ratio": { + "label": "防皱收尾比率", + "doc": "周期必须走到匹配程序预期时长的多大比例,WashData 才可以判定当前看到的低功率翻滚是防皱尾段并就此结束周期。预期时长是该程序的平均值,因此对于按衣物量决定传感烘干时间的烘干机,大多数运行都达不到它的 98%,尾段会一直挂到备用超时才结束,常常比机器实际停止晚 15 到 40 分钟。这类烘干机请调低此值(例如 0.75)。洗衣机请保持默认值:洗衣机的大部分周期都低于“最大防皱功率”,因此这个比例是区分安静的洗涤中段与尾段的唯一依据,调低它可能会把一次洗涤从中间截断。留空表示使用默认值(0.98)。本设置与“智能终止比率”不同,后者控制的是另一项提前结束检查。" + }, + "curve_preroll_seconds": { + "label": "曲线前导采样", + "doc": "WashData 最多可以把中止的启动尝试中多少秒的读数并入周期曲线的开头。周期曲线通常从最终成功的那次启动尝试开始,因此对于需要试探几次才稳定下来的机器(选择程序、门锁、首次进水),每条曲线都会缺少真实运行的最初几分钟。请把此值设得略大于您的机器最长的一次起步过程(多数情况下 300 秒绰绰有余)。0 表示关闭。请注意,开启后记录的开始时间会提前,因此在更改前后记录的周期,即使是同一个程序,时长也会不同;在较早的周期被淘汰之前,学习到的平均值会一直存在偏差。" } }, "setting_group": { @@ -1728,7 +1766,8 @@ "splitting": "拆分中…", "deleting": "删除中…", "imported": "已导入", - "preparing": "正在准备…" + "preparing": "正在准备…", + "unlabelling": "取消标记中…" }, "tab": { "advanced": "高级", @@ -1808,6 +1847,7 @@ "recording_stopped": "录制已停止", "review_saved": "审查已保存", "settings_conflicts": "请先解决标红的设置冲突,再保存。", + "settings_no_changes": "没有需要保存的更改", "settings_reverted": "设置已恢复;集成正在重新加载", "settings_saved": "设置已保存;集成重载中", "suggestions_applied": "建议已应用;集成重载中", @@ -1866,7 +1906,11 @@ "export_selective_done": "导出已下载", "import_selective_done": "已导入 {profiles} 个配置文件和 {cycles} 个周期", "hist_csv_required": "请先加载 CSV 文件或粘贴其内容", - "file_read_failed": "无法读取该文件" + "file_read_failed": "无法读取该文件", + "unlabel_done": "已取消标记 {count} 个周期", + "unlabel_failed": "取消标记失败:{error}", + "odometer_saved": "已保存累计周期数", + "odometer_save_failed": "无法保存累计值:{error}" }, "suggestion": { "both_agree": "WashData建议", @@ -1913,10 +1957,6 @@ "higher": "仅提交高置信度匹配,更多循环保持未标记", "lower": "匹配更多循环,部分可能略有偏差" }, - "running_dead_zone": { - "higher": "忽略待机时的短暂功率尖峰––对噪声不那么敏感", - "lower": "响应更短的功率突发––捕捉快速瞬态活动" - }, "start_threshold_w": { "higher": "避免因短暂功率尖峰引起的误启动", "lower": "捕捉低功率程序,可能对噪声触发" @@ -1962,7 +2002,9 @@ "thr_batch": "保持在 {cycles} 个周期中 p05 最低工作功率({p05}W)略上方,以便尽早捕捉到启动,同时使停止阈值保持在设备最低运行功率之下。", "tol_per_profile": "{profiles} 个配置文件({cycles} 个周期)中每个配置文件时长方差的 p75;波动小的配置文件不受惩罚。", "tol_pooled": "基于 {cycles} 个近期已标注周期的合并时长方差(p95 偏差={dev})。", - "watchdog": "在安全范围内保持尽可能低(略高于 p95 更新间隔 {p95}s,且至少为采样间隔 {median}s 的 2 倍,最小 30s),以便快速捕捉到停滞而不产生误停止。" + "watchdog": "在安全范围内保持尽可能低(略高于 p95 更新间隔 {p95}s,且至少为采样间隔 {median}s 的 2 倍,最小 30s),以便快速捕捉到停滞而不产生误停止。", + "match_interval_capped": "已设上限,使 {persistence} 次连续匹配能落在最短程序({shortest}s)的 {pct}% 之内;仅按更新频率(中位数={median}s)计算本会建议更长的间隔。", + "match_interval_floored": "仅按最短程序({shortest} 秒)计算,上限会是 {cap} 秒,低于 {minimum} 秒的最小值,因此保持在最小值:{persistence} 次连续匹配需要 {budget} 秒,超过该程序的 {pct}%。匹配只在有读数到达时才运行(中位数={median} 秒),因此使用最小值不会带来额外开销。" }, "exclusions": { "summary": "已排除 {total} 个误检测周期:{parts}。", @@ -1998,10 +2040,11 @@ "date_tip": "周期开始的日期和时间。", "duration_tip": "从开始到结束的总周期运行时间。", "energy_tip": "消耗的总能量(kWh)。通过对功率随时间积分计算得出。", - "cost_tip": "此周期的能耗成本,在完成时按当时生效的价格冻结(能量 x 每 kWh 价格)。在设置中设定价格以填充此项。", + "cost_tip": "此周期的能耗成本,在完成时冻结。启用“时间加权成本”后,功率曲线按各时刻生效的电价计费;否则为能量 x 结束时的单一价格。在设置中设定价格以填充此项。", "confidence_tip": "配置文件匹配置信度(0-100%)。周期功率曲线与识别出的程序的匹配程度。", "health_tip": "ML 周期健康(越高越好)。点击周期以检查并审查。", - "flags_tip": "周期的审核、异常和来源标志。将鼠标悬停在图标上可查看详细信息。" + "flags_tip": "周期的审核、异常和来源标志。将鼠标悬停在图标上可查看详细信息。", + "cost_dynamic_tip": "时间加权:按周期运行期间生效的电价计费(实际 {price} {cur}/kWh)。" }, "stat": { "avg": "平均", @@ -2028,9 +2071,9 @@ "finished": "循环到达终止状态并已结束。" }, "store": { + "search_ph2": "按品牌或型号搜索…", "your_model_tip": "这是您在设置中指定的家电", "your_model": "您的型号", - "search_brand_ph": "按品牌搜索…", "programs_count": "程序:{n}", "browse": "浏览", "device": "设备", @@ -2075,7 +2118,8 @@ "golden": "重新处理:补全参考周期", "suggestions": "重新处理:建议", "ml_training": "重新处理:ML 训练", - "health": "重新处理:周期健康" + "health": "重新处理:周期健康", + "costs": "重新处理:能耗成本" }, "pg_sweep": { "optimize": "优化:{param}" diff --git a/custom_components/ha_washdata/ws_api.py b/custom_components/ha_washdata/ws_api.py index 7b23e6df..f15d4558 100644 --- a/custom_components/ha_washdata/ws_api.py +++ b/custom_components/ha_washdata/ws_api.py @@ -43,6 +43,10 @@ from .const import ( CONF_DEVICE_TYPE, CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE, CONF_SMART_TERMINATION_DURATION_RATIO, + CONF_ANTI_CREASE_FINALIZE_RATIO, + ANTI_CREASE_FINALIZE_RATIO_MIN, + ANTI_CREASE_FINALIZE_RATIO_MAX, + CONF_CURVE_PREROLL_SECONDS, CONF_DOOR_SENSOR_ENTITY, CONF_ANTI_WRINKLE_EXIT_POWER, CONF_ANTI_WRINKLE_MAX_POWER, @@ -78,6 +82,9 @@ from .const import ( DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS, DEFAULT_SMART_TERMINATION_DURATION_RATIO, DEFAULT_SMART_TERMINATION_DURATION_RATIO_BY_DEVICE, + DEFAULT_ANTI_CREASE_FINALIZE_RATIO, + DEFAULT_CURVE_PREROLL_SECONDS, + CURVE_PREROLL_MAX_SECONDS, resolve_sampling_interval_default, resolve_watchdog_interval_default, resolve_start_duration_default, @@ -108,7 +115,11 @@ from .const import ( from . import history_import from . import playground from . import task_registry -from .cycle_detector import CycleDetectorConfig +from .cycle_detector import ( + CycleDetectorConfig, + effective_anticrease_finalize_ratio, + effective_curve_preroll_seconds, +) from .options_utils import strip_null_options from .setup_advisor import compute_setup_phase from .ws_schema import WS_OPEN_RESPONSES, WS_RESPONSE_TYPES @@ -540,6 +551,8 @@ _FULL_COMMANDS = frozenset({ "get_export_inventory", "analyze_import", "export_config_selective", "import_config_selective", # Reverting on-device models / matcher tuning discards learned state -> full access. "revert_matching_config", "revert_ml_models", + # Rewrites the appliance's lifetime odometer, which drives maintenance schedules. + "set_lifetime_cycle_count", # Historical power-data import: ingests a whole power history and writes cycles. "history_import_begin", "history_import_chunk", "history_import_recorder", "start_history_import_scan", "apply_history_import", @@ -1222,6 +1235,7 @@ def async_register_commands(hass: HomeAssistant) -> None: ws_get_profile_groups, ws_save_profile_group, ws_rename_profile_group, ws_delete_profile_group, # Maintenance log (Group E) ws_get_maintenance_log, ws_add_maintenance_event, ws_delete_maintenance_event, + ws_set_lifetime_cycle_count, # Cycles ws_label_cycle, ws_delete_cycle, ws_auto_label_cycles, # Phase catalog @@ -1340,6 +1354,12 @@ def ws_get_devices( "recording": False, "is_user_paused": False, "manual_program": False, + "armed_program": None, + # Defaulted here rather than only in the manager branch below: both are + # declared non-optional by DeviceInfo (ws_schema.py) and ws-types.d.ts, + # and the branch is skipped for a manager-less entry (mid-setup, stale, + # or a setup that failed) as well as short-circuited by its own except. + "envelope_position": None, "options": dict(entry.options), # Device-resolved defaults for the cadence/ratio fields (#396/#393) so the # device-list conflict/suggestion badges score an unset field against the @@ -1361,17 +1381,29 @@ def ws_get_devices( program = None info["current_program"] = program info["manual_program"] = bool(getattr(manager, "manual_program_active", False)) + # A program pinned for the NEXT cycle (#411). Kept separate from + # current_program so an idle device does not claim to be running one. + info["armed_program"] = getattr(manager, "armed_program", None) info["time_remaining_s"] = getattr(manager, "_time_remaining", None) info["total_duration_s"] = getattr(manager, "_total_duration", None) - power = getattr(manager, "_current_power", None) + # Read through the property, not the raw cache (#409): it falls back + # to the sensor's live state so the panel can never show a power the + # sensor never reported. + power = getattr(manager, "current_power", None) info["current_power_w"] = round(float(power), 2) if power is not None else None progress = getattr(manager, "_cycle_progress", None) if progress is not None: info["cycle_progress_pct"] = round(float(progress), 1) + # Where the run maps onto the matched profile's own curve (item 269). + # Independent of elapsed time, so it stays meaningful when a cycle + # over- or under-runs; refreshed only during low-power phases, which + # the panel's tooltip says. + info["envelope_position"] = getattr(manager, "envelope_position", None) + store = getattr(manager, "profile_store", None) if store is not None: try: @@ -1518,6 +1550,10 @@ def _resolved_option_defaults(device_type: str) -> dict[str, Any]: CONF_SMART_TERMINATION_DURATION_RATIO: ( resolve_smart_termination_duration_ratio_default(device_type) ), + # #429: deliberately NOT device-resolved (the safe value is a property of + # the individual machine), but the panel pre-populates it from the same + # payload, so it is published here rather than left to the JS default. + CONF_ANTI_CREASE_FINALIZE_RATIO: DEFAULT_ANTI_CREASE_FINALIZE_RATIO, } @@ -1535,7 +1571,14 @@ def ws_get_options( if not entry: connection.send_error(msg["id"], "not_found", f"Entry {msg['entry_id']!r} not found") return - options = {**entry.data, **entry.options} + # #422: the display name is carried by ``entry.title``, never by options - + # ``_merge_structural_options`` and ``_OPTIONS_IDENTITY_KEYS`` strip CONF_NAME + # back out on every save. ``entry.data[CONF_NAME]`` is therefore a fossil from + # ``async_create_entry`` that no rename path updates, and being data-only it is + # the one key nothing in options can shadow. Pin it to the title so Settings -> + # Basic shows the same name as the device list (which serves entry.title) and a + # save can never echo the creation-time name back over the current one. + options = {**entry.data, **entry.options, CONF_NAME: entry.title} # Device-resolved defaults for the cadence settings whose defaults vary by # device type (#396). The panel uses these as the render/conflict-check # fallback for an unset field so it shows (and validates against) the value the @@ -1639,6 +1682,48 @@ async def ws_set_options( except (TypeError, ValueError): new_options.pop(CONF_SMART_TERMINATION_DURATION_RATIO, None) + # #429: the anti-crease finalise ratio is the same kind of value against the + # same kind of mean, and equally meaningless outside [0.50, 1.00]. An empty or + # non-numeric submission drops the key so DEFAULT_ANTI_CREASE_FINALIZE_RATIO + # applies again. + if CONF_ANTI_CREASE_FINALIZE_RATIO in new_options: + _raw_ac = new_options[CONF_ANTI_CREASE_FINALIZE_RATIO] + if _raw_ac in (None, ""): + new_options.pop(CONF_ANTI_CREASE_FINALIZE_RATIO, None) + else: + try: + _ac = float(_raw_ac) + if not math.isfinite(_ac): + raise ValueError("non-finite") + new_options[CONF_ANTI_CREASE_FINALIZE_RATIO] = min( + ANTI_CREASE_FINALIZE_RATIO_MAX, + max(ANTI_CREASE_FINALIZE_RATIO_MIN, _ac), + ) + # OverflowError too (register item 194): json parses an integer literal + # of any length into an unbounded int, and float() on one of those raises + # rather than returning inf. Uncaught it becomes ERR_UNKNOWN_ERROR and + # the whole save fails, instead of this key falling back to its default. + except (TypeError, ValueError, OverflowError): + new_options.pop(CONF_ANTI_CREASE_FINALIZE_RATIO, None) + + # #430: seconds, 0 = off. Clamped to [0, CURVE_PREROLL_MAX_SECONDS] so a + # mistyped value cannot drag minutes of unrelated standby into a curve; empty + # or non-numeric drops the key and restores the default (off). + if CONF_CURVE_PREROLL_SECONDS in new_options: + _raw_pr = new_options[CONF_CURVE_PREROLL_SECONDS] + if _raw_pr in (None, ""): + new_options.pop(CONF_CURVE_PREROLL_SECONDS, None) + else: + try: + _pr = float(_raw_pr) + if not math.isfinite(_pr): + raise ValueError("non-finite") + new_options[CONF_CURVE_PREROLL_SECONDS] = min( + CURVE_PREROLL_MAX_SECONDS, max(0.0, _pr) + ) + except (TypeError, ValueError, OverflowError): + new_options.pop(CONF_CURVE_PREROLL_SECONDS, None) + # A None outside the clearable selectors means "not set", not a value: the # per-setting Revert sends the changelog's `old`, which is null for a setting # never saved before. Stored, it would survive options.get(key, DEFAULT) and @@ -1856,12 +1941,33 @@ async def ws_get_profiles( except Exception: # pylint: disable=broad-exception-caught pass + # How each programme ENDS, measured from its own cycles. Read-only: no + # detection path consults it, and `consistency` is there to be read first, + # because the event it describes is present in only some runs of the same + # programme (register item 238). + terminal: dict[str, Any] = {} + try: + for _profile in profiles: + # `profiles` is list_profiles(): a list of profile DICTS, not names. + # Passing the dict straight in compared it against each cycle's + # `profile_name` string, which can never match, so the feature + # returned {} for every device while raising nothing. + _name = _profile.get("name") if isinstance(_profile, dict) else None + if not _name: + continue + _sig = manager.profile_store.compute_profile_terminal_signature(_name) + if _sig is not None: + terminal[_name] = _sig + except Exception: # pylint: disable=broad-exception-caught + terminal = {} + return { "profiles": profiles, "profile_health": health, "profile_trends": trends, "coverage_gaps": coverage_gaps, "profile_advisories": advisories, + "profile_terminal": terminal, } stats = await hass.async_add_executor_job(_compute_stats) @@ -2266,14 +2372,126 @@ async def ws_get_maintenance_log( cfg = manager.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES) if isinstance(cfg, dict): reminders.update(cfg) + store = manager.profile_store _send_result(connection, msg["id"], "get_maintenance_log", { - "log": manager.profile_store.get_maintenance_log(), + "log": store.get_maintenance_log(), "due": manager.maintenance_due, "event_types": list(MAINTENANCE_EVENT_TYPES), "reminders": reminders, + # Cycles run since each task was last done, and the odometer they are + # measured against (#414). Computed backend-side before this and never + # sent, so the panel could show "due" but never "how close". + "cycles_since": { + evt: store.cycles_since_maintenance(evt) for evt in MAINTENANCE_EVENT_TYPES + }, + "lifetime_cycle_count": manager.lifetime_cycle_count, }) +@websocket_api.websocket_command( + { + vol.Required("type"): "ha_washdata/set_lifetime_cycle_count", + vol.Required("entry_id"): str, + vol.Required("count"): vol.All(vol.Coerce(int), vol.Range(min=0, max=1000000)), + } +) +@websocket_api.async_response +async def ws_set_lifetime_cycle_count( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Correct the appliance's lifetime cycle odometer (requires full access). + + The one sanctioned way the count moves other than a cycle completing (#414). + It exists because WashData cannot always tell a real short run from an + artefact, and because an appliance may have run for years before WashData was + installed - rather than asking "was that a real cycle?" on every deletion, the + user corrects the total once. Recorded in the settings changelog. + """ + entry_id: str = msg["entry_id"] + manager = _get_manager(hass, entry_id) + if manager is None: + _err_not_found(connection, msg["id"], entry_id) + return + try: + store = manager.profile_store + count = int(msg["count"]) + # Serialize the whole read-check-mutate-save-rollback under the per-entry + # write lock. The rollback below restores the value read at the start, so two + # corrections that interleave across the save could see the earlier one's + # failure undo the later one's success (A reads 10, B reads and writes 4000 + # successfully, A's save fails and A restores 10). Holding the lock across the + # await is the point; taking it only around the mutation would not help. + async with _entry_write_lock(hass, msg["entry_id"]): + previous = store.get_lifetime_cycle_count() + # A stored record is evidence of a run, so the odometer can never read below + # the number of records on hand - that floor is what get_lifetime_cycle_count + # applies. Without this check the setter accepted a lower value, the getter + # masked it while the history was long, and it then surfaced later once + # records were deleted: a correction that appeared to do nothing and took + # effect retroactively, i.e. the odometer regression #414 exists to prevent. + # Refused with the floor named rather than silently clamped, so the user is + # told why their number was not taken. + floor = len(store.get_past_cycles()) + if count < floor: + connection.send_error( + msg["id"], + "invalid_format", + f"Cannot set the lifetime count below the {floor} cycle records " + f"currently stored; delete records first or choose {floor} or more", + ) + return + store.set_lifetime_cycle_count(count, force=True) + try: + # ONE save for both mutations: async_record_settings_changes persists the + # store, and the new count is already staged in memory. Saving separately + # beforehand meant a changelog failure reported unknown_error on a + # correction that had in fact been written, and left `previous` reading + # the new value on a retry - recording old == new. + await store.async_record_settings_changes( + [{"key": "lifetime_cycle_count", "old": previous, "new": count}] + ) + except Exception: + # Undo only OUR write. A cycle completing during the await bumps the + # same counter (manager._async_process_cycle_end -> lifetime energy + # save), and that path deliberately does not take this lock: it lives + # in the manager, on the hot cycle-end path, and reaching into the WS + # layer's lock from there would invert the layering for a window this + # narrow. Compare-and-swap keeps the rollback honest without it - if + # the value is no longer what we wrote, someone else owns it now and + # restoring `previous` would discard their increment. + if store.get_lifetime_cycle_count() == count: + store.set_lifetime_cycle_count(previous, force=True) + raise + # Re-validate the manager is still live after the awaited save: a reload + # during it detaches this manager (and its per-entry lock, which + # async_unload_entry pops), so notifying it would target stale state and + # reporting its store's count would show the panel a number from a store + # nothing reads any more. Mirrors the guard the recording-persist and import + # handlers use. Reported as success because the save itself did happen; the + # count comes from whatever store is live now. + current_manager = _get_manager(hass, entry_id) + if current_manager is not manager: + _LOGGER.warning( + "Manager replaced during lifetime-count correction for %s; " + "skipping notify", entry_id, + ) + else: + manager.notify_update() + live_store = ( + current_manager.profile_store if current_manager is not None else store + ) + _send_result( + connection, + msg["id"], + "set_lifetime_cycle_count", + {"success": True, "lifetime_cycle_count": live_store.get_lifetime_cycle_count()}, + ) + except Exception as exc: # pylint: disable=broad-exception-caught + connection.send_error(msg["id"], "unknown_error", str(exc)) + + @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/add_maintenance_event", @@ -3025,7 +3243,7 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None reg.finish(task, state=task_registry.STATE_CANCELLED) return - reg.update(task, total=5, done=0, label="Reprocessing: matching cycles", + reg.update(task, total=6, done=0, label="Reprocessing: matching cycles", label_key="task.reprocess.matching") summary["count"] = await store.async_reprocess_all_data() @@ -3071,7 +3289,21 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None reg.finish(task, state=task_registry.STATE_CANCELLED, result=summary) return - reg.update(task, done=4, label="Reprocessing: cycle health", + reg.update(task, done=4, label="Reprocessing: energy costs", + label_key="task.reprocess.costs") + # Recost stored cycles against the recorder's price history (#426). Cheap + # and a no-op unless a dynamic price entity is configured, so it runs + # unconditionally rather than behind yet another switch. + try: + summary["costs_recomputed"] = await manager.async_recompute_cycle_costs() + except Exception as exc: # pylint: disable=broad-exception-caught + _LOGGER.debug("cost recompute failed for %s: %s", entry_id, exc) + + if task.cancel_requested: + reg.finish(task, state=task_registry.STATE_CANCELLED, result=summary) + return + + reg.update(task, done=5, label="Reprocessing: cycle health", label_key="task.reprocess.health") # Recompute per-cycle health against the (possibly retrained) model. # Skip when training already recomputed it (a promotion refreshes health). @@ -3081,7 +3313,7 @@ async def _reprocess_task(hass: HomeAssistant, task: Any, entry_id: str) -> None except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("health recompute failed for %s: %s", entry_id, exc) - reg.update(task, done=5) + reg.update(task, done=6) # Re-validate the manager is still live after a long chain of awaits; if the # entry was reloaded, the original manager is detached and must not notify. if _get_manager(hass, entry_id) is manager: @@ -3112,7 +3344,8 @@ def ws_reprocess_history( """Kick off the full "Process history" pass as a detached, registry-tracked task; returns its id immediately. Runs, in order: reprocess (rematch + rebuild envelopes) -> backfill golden -> refresh suggestions -> on-device ML training - (when enabled) -> recompute cycle health. Progress + result via the registry.""" + (when enabled) -> recost cycles from recorder price history -> recompute cycle + health. Progress + result via the registry.""" entry_id: str = msg["entry_id"] if _get_manager(hass, entry_id) is None: _err_not_found(connection, msg["id"], entry_id) @@ -3166,6 +3399,10 @@ async def ws_wipe_history( try: await manager.profile_store.clear_all_data() + # clear_all_data pops the persisted arm, but the manager's in-memory field is + # the authoritative one, so the pin has to be retired there too or the next + # cycle re-applies a program from before the wipe. + manager.clear_armed_program() manager.notify_update() _send_result(connection, msg["id"], "wipe_history", {"success": True}) except Exception as exc: # pylint: disable=broad-exception-caught @@ -4481,8 +4718,16 @@ def ws_set_program( prog = msg.get("program") if not prog or prog in ("auto_detect", "__auto__", "none"): manager.clear_manual_program() - else: - manager.set_manual_program(prog) + elif not manager.set_manual_program(prog): + # Only a program that does not exist can fail now (#411). It used to + # fail for the far more common reason of no cycle being under way, and + # this line reported success anyway because the manager returned None + # either way, so the panel showed a confirmation and then quietly + # reverted the dropdown. + connection.send_error( + msg["id"], "not_found", f"No such program: {prog}" + ) + return manager.notify_update() _send_result(connection, msg["id"], "set_program", {"success": True}) except Exception as exc: # pylint: disable=broad-exception-caught @@ -5539,6 +5784,16 @@ def _playground_base_config(manager: Any, entry: Any) -> CycleDetectorConfig: str(opts.get(CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE)) ), ), + # The same helpers the detector reads these through, not a bare float: + # this branch builds the sim config when the live detector is unavailable, + # and an imported or stale option would otherwise give the Playground a + # value production would have clamped (register items 244, 249). + anti_crease_finalize_ratio=effective_anticrease_finalize_ratio( + opts.get(CONF_ANTI_CREASE_FINALIZE_RATIO, DEFAULT_ANTI_CREASE_FINALIZE_RATIO) + ), + curve_preroll_seconds=effective_curve_preroll_seconds( + opts.get(CONF_CURVE_PREROLL_SECONDS, DEFAULT_CURVE_PREROLL_SECONDS) + ), # Match the live detector's tuned gate, not the dataclass 0.4, so the sim's # Smart-Termination / anti-crease confidence checks reproduce production. match_confidence_threshold=_safe_float_finite( diff --git a/custom_components/ha_washdata/ws_schema.py b/custom_components/ha_washdata/ws_schema.py index 4307d810..22b0a75b 100644 --- a/custom_components/ha_washdata/ws_schema.py +++ b/custom_components/ha_washdata/ws_schema.py @@ -79,12 +79,14 @@ class DeviceInfo(TypedDict): total_duration_s: float | None current_power_w: float | None cycle_progress_pct: float | None + envelope_position: float | None suggestions_count: int suggestion_keys: list[str] feedback_count: int recording: bool is_user_paused: bool manual_program: bool + armed_program: str | None options: dict[str, Any] # Device-resolved defaults for the cadence/ratio fields whose default varies by # device type (#396/#393), so the device-list conflict/suggestion badges can score @@ -127,6 +129,7 @@ class GetProfilesResponse(TypedDict): profile_trends: dict[str, Any] coverage_gaps: dict[str, Any] profile_advisories: list[dict[str, Any]] + profile_terminal: dict[str, Any] class CreateProfileResponse(TypedDict): @@ -158,6 +161,13 @@ class GetMaintenanceLogResponse(TypedDict): due: Any event_types: list[str] reminders: dict[str, Any] + cycles_since: dict[str, int] + lifetime_cycle_count: int + + +class SetLifetimeCycleCountResponse(TypedDict): + success: bool + lifetime_cycle_count: int class AddMaintenanceEventResponse(TypedDict): @@ -736,6 +746,7 @@ WS_RESPONSE_TYPES: dict[str, type] = { "get_maintenance_log": GetMaintenanceLogResponse, "add_maintenance_event": AddMaintenanceEventResponse, "delete_maintenance_event": SuccessResponse, + "set_lifetime_cycle_count": SetLifetimeCycleCountResponse, "label_cycle": SuccessResponse, "delete_cycle": SuccessResponse, "auto_label_cycles": SuccessResponse, @@ -926,6 +937,7 @@ WS_COMMANDS: dict[str, dict] = { _p("notes", "str", False), ]}, "delete_maintenance_event": {"params": [_entry(), _p("event_id", "str")]}, + "set_lifetime_cycle_count": {"params": [_entry(), _p("count", "int")]}, "label_cycle": {"params": [ _entry(), _p("cycle_id", "str"), diff --git a/custom_components/ha_washdata/www/build-manifest.json b/custom_components/ha_washdata/www/build-manifest.json index 2f70ada9..32786b1d 100644 --- a/custom_components/ha_washdata/www/build-manifest.json +++ b/custom_components/ha_washdata/www/build-manifest.json @@ -3,17 +3,17 @@ "assets": { "ha-washdata-panel.js": { "artifact": "ha-washdata-panel.min.js", - "source_sha256": "b7065b51086421552eecaa395f8dfd743b2f0b646f9ad80a5e3e955dc54b5926", - "artifact_sha256": "92d975bc6c11acff7060c2bd11d1ad09b1c43b0575e1655d7bec67a9438cd327", - "source_bytes": 843545, - "artifact_bytes": 564506 + "source_sha256": "56d8a3b1f6637b622d6300df1320d962b7399a93bf66b42ffb73bc32baf14b47", + "artifact_sha256": "06916396cf1974fd4266d24e6f0678375031d220634d377b6cda39d0eb6d3a34", + "source_bytes": 917813, + "artifact_bytes": 597157 }, "ha-washdata-card.js": { "artifact": "ha-washdata-card.min.js", - "source_sha256": "71a6d762af2ab78525a3bc169d6bfa682ce5f79ab3bd23a808059102a1d223e3", - "artifact_sha256": "f7e8ab6492010f5ad6875e0c2969a5fe43ff3040ac96ada665cd61016963ea16", - "source_bytes": 50627, - "artifact_bytes": 29435 + "source_sha256": "a4c8d484bf7fdbdf5d5eb82d8f31fd7d3c73bc22f2ab16e5f2c374db528baa8e", + "artifact_sha256": "0c344e93071a232b1622a62272547556a301c78e1cb25b94b9c755bb9bd34fd8", + "source_bytes": 55871, + "artifact_bytes": 30307 } } } diff --git a/custom_components/ha_washdata/www/ha-washdata-card.js b/custom_components/ha_washdata/www/ha-washdata-card.js index 82d470a6..e67129a7 100644 --- a/custom_components/ha_washdata/www/ha-washdata-card.js +++ b/custom_components/ha_washdata/www/ha-washdata-card.js @@ -54,6 +54,11 @@ const BUTTON_ICONS = { const BUTTON_ORDER = ["pause", "resume", "terminate", "record_start", "record_stop", "program", "open_panel"]; const ACTIVE_STATES = ["running", "paused", "user_paused", "ending", "starting", "anti_wrinkle", "rinse"]; +// States where button.py's PauseCycleButton is available, so the card must not +// grey out Pause where the backend would accept it. An auto-detected "paused" +// is pausable: async_pause_cycle turns it into a user pause that survives the +// power drop. +const PAUSABLE_STATES = ["running", "starting", "paused", "ending"]; const INACTIVE_STATES = ["off", "unknown", "unavailable", "idle"]; const SPARK_MAX_POINTS = 60; @@ -878,8 +883,8 @@ class WashDataCard extends HTMLElement { if (!acts) return; const sk = vm.sk; const enabled = { - pause: sk === "running", - resume: sk === "paused" || sk === "user_paused", + pause: PAUSABLE_STATES.includes(sk), + resume: sk === "user_paused", terminate: vm.isActive, record_start: !vm.isActive, record_stop: true, @@ -1016,7 +1021,7 @@ class WashDataCard extends HTMLElement { const hass = this._hass; if (!hass) return; if (b === "open_panel") { - this._navigate("/" + "ha-washdata"); + this._navigate("/" + "ha-washdata" + this._deepLinkQuery()); return; } const roles = this._resolveRoles(); @@ -1040,6 +1045,34 @@ class WashDataCard extends HTMLElement { this.dispatchEvent(new CustomEvent("hass-more-info", { detail: { entityId }, bubbles: true, composed: true })); } + // ── Panel deep link (#438) ───────────────────────────────────────────────── + // "Open WashData" used to land on whichever appliance the panel showed last, so + // the dryer's card opened the washer. Carry this card's own appliance as + // ?device=. The config entry id is preferred because it is the only token that + // survives a rename; the device name is the documented fallback and the panel + // resolves it case- and accent-insensitively. Returns "" when neither is + // knowable, which simply restores the old behaviour for that card. + _deepLinkQuery() { + const hass = this._hass; + const cfg = this._cfg || {}; + const reg = (hass && hass.entities) || {}; + const primary = cfg.entity; + const primaryReg = reg[primary]; + const deviceId = cfg.device_id || (primaryReg && primaryReg.device_id) || null; + const dev = (deviceId && hass && hass.devices) ? hass.devices[deviceId] : null; + let token = ""; + // Only read an entry id off a device we actually own: a card pointed at a + // template or third-party entity would otherwise hand the panel the wrong + // integration's entry id, which resolves to nothing. + if (dev && primaryReg && primaryReg.platform === DOMAIN) { + const entries = Array.isArray(dev.config_entries) ? dev.config_entries : []; + token = dev.primary_config_entry || (entries.length === 1 ? entries[0] : ""); + } + if (!token && dev) token = dev.name_by_user || dev.name || ""; + if (!token && primary) token = this._deviceNameFor(primary); + return token ? "?device=" + encodeURIComponent(token) : ""; + } + _navigate(path) { window.history.pushState(null, "", path); window.dispatchEvent(new CustomEvent("location-changed", { detail: { replace: false } })); @@ -1311,14 +1344,77 @@ class WashDataCardEditor extends HTMLElement { } } -customElements.define(CARD_TAG, WashDataCard); -customElements.define(EDITOR_TAG, WashDataCardEditor); +// Element registration, made resilient against the scoped-registry race (#384). +// +// Home Assistant starts this module in one of two ways. A dashboard whose +// resources live in the UI registry imports it from inside app.js, so it always +// runs AFTER the frontend installed @webcomponents/scoped-custom-element-registry. +// With `lovelace: resource_mode: yaml` that collection is read-only, so the +// integration falls back to add_extra_js_url() (see frontend.py::_init_resource) +// and the browser starts this module as its own